Firebase を Next.js アプリと統合する

1. 始める前に

この Codelab では、Firebase をレストランのレビュー ウェブサイトである Friendly Eats という Next.js ウェブアプリと統合する方法について学びます。

フレンドリー イーツ ウェブアプリ

完成したウェブアプリには、Firebase を使用して Next.js アプリを構築する方法を示した便利な機能が備わっています。主な機能は次のとおりです。

  • 自動ビルドとデプロイ: この Codelab では、Firebase App Hosting を使用して、構成したブランチに push するたびに Next.js コードを自動的にビルドしてデプロイします。
  • ログインとログアウト: 完成したウェブアプリでは、Google でログインしたりログアウトしたりできます。ユーザーのログインと永続性は、すべて Firebase Authentication で管理されます。
  • 画像: 完成したウェブアプリでは、ログインしたユーザーがレストランの画像をアップロードできます。画像アセットは Cloud Storage for Firebase に保存されます。Firebase JavaScript SDK は、アップロードされた画像のパブリック URL を提供します。この公開 URL は、Cloud Firestore の関連するレストラン ドキュメントに保存されます。
  • レビュー: 完成したウェブアプリでは、ログインしたユーザーがレストランのレビューを投稿できます。レビューは、星評価とテキスト ベースのメッセージで構成されます。レビュー情報は Cloud Firestore に保存されます。
  • フィルタ: 完成したウェブアプリでは、ログインしたユーザーがカテゴリ、場所、価格に基づいてレストランのリストをフィルタできます。使用する並べ替え方法をカスタマイズすることもできます。データは Cloud Firestore からアクセスされ、使用されるフィルタに基づいて Firestore クエリが適用されます。

前提条件

  • GitHub アカウント
  • Next.js と JavaScript に関する知識

学習内容

  • Next.js App ルーターとサーバーサイド レンダリングで Firebase を使用する方法。
  • Cloud Storage for Firebase に画像を保持する方法。
  • Cloud Firestore データベースでデータの読み取りと書き込みを行う方法。
  • Firebase JavaScript SDK で「Google でログイン」を使用する方法。

必要なもの

  • Git
  • Node.js の最新の安定版
  • 任意のブラウザ(Google Chrome など)
  • コードエディタとターミナルを備えた開発環境
  • Firebase プロジェクトの作成と管理を行う Google アカウント
  • Firebase プロジェクトを Blaze お支払いプランにアップグレードする機能

2. 開発環境と GitHub リポジトリを設定する

この Codelab では、アプリのスターター コードベースを提供し、Firebase CLI を使用します。

GitHub リポジトリを作成する

Codelab のソースは https://github.com/firebase/friendlyeats-web にあります。このリポジトリには、複数のプラットフォーム用のサンプル プロジェクトが含まれています。ただし、この Codelab では nextjs-start ディレクトリのみを使用します。次のディレクトリをメモします。

* `nextjs-start`: contains the starter code upon which you build.
* `nextjs-end`: contains the solution code for the finished web app.

nextjs-start フォルダを独自のリポジトリにコピーします。

  1. ターミナルを使用して、コンピューターに新しいフォルダを作成し、新しいディレクトリに移動します。
    mkdir codelab-friendlyeats-web
    
    cd codelab-friendlyeats-web
    
  2. giget npm パッケージを使用して、nextjs-start フォルダのみを取得します。
    npx giget@latest gh:firebase/friendlyeats-web/nextjs-start#master . --install
    
  3. git を使用してローカルで変更を追跡します。
    git init
    
    git commit -a -m "codelab starting point"
    
    git branch -M main
    
  4. 新しい GitHub リポジトリ(https://github.com/new)を作成します。任意の名前を付けます。
    1. GitHub から、https://github.com//.git または git@github.com:/.git のような新しいリポジトリ URL が提供されます。この URL をコピーします。
  5. ローカルの変更を新しい GitHub リポジトリに push します。 プレースホルダにリポジトリ URL を挿入して、次のコマンドを実行します。
    git remote add origin <your-repository-url>
    
    git push -u origin main
    
  6. GitHub リポジトリにスターター コードが表示されます。

Firebase CLI をインストールまたは更新する

次のコマンドを実行して、Firebase CLI がインストールされ、v13.9.0 以降であることを確認します。

firebase --version

バージョンが低い場合や Firebase CLI がインストールされていない場合は、インストール コマンドを実行します。

npm install -g firebase-tools@latest

権限エラーが原因で Firebase CLI をインストールできない場合は、npm のドキュメントをご覧ください。または、別のインストール オプションを使用してください。

Firebase にログインする

  1. 次のコマンドを実行して Firebase CLI にログインします。
    firebase login
    
  2. Firebase でデータを収集するかどうかに応じて、Y または N を入力します。
  3. ブラウザで Google アカウントを選択し、[許可] をクリックします。

3. Firebase プロジェクトを設定する

このセクションでは、Firebase プロジェクトを設定し、Firebase ウェブアプリを関連付けます。また、サンプル ウェブアプリで使用される Firebase サービスを設定します。

Firebase プロジェクトを作成する

  1. Firebase コンソールで [プロジェクトを追加] をクリックします。
  2. [Enter your project name] テキスト ボックスに「FriendlyEats Codelab」(または任意のプロジェクト名)と入力し、[続行] をクリックします。
  3. [Firebase の料金プランの確認] モーダルで、プランが Blaze であることを確認して、[プランを確認] をクリックします。
  4. この Codelab では Google アナリティクスは必要ないため、[このプロジェクトで Google アナリティクスを有効にする] オプションをオフにします。
  5. [プロジェクトの作成] をクリックします。
  6. プロジェクトがプロビジョニングされるのを待ってから、[続行] をクリックします。
  7. Firebase プロジェクトで、[プロジェクトの設定] に移動します。後で必要になるため、プロジェクト ID をメモしておきます。この一意の識別子によって、プロジェクトが識別されます(Firebase CLI など)。

Firebase の料金プランをアップグレードする

Firebase App Hosting と Cloud Storage for Firebase を使用するには、Firebase プロジェクトが従量課金制(Blaze)のお支払いプランCloud 請求先アカウントにリンクされている)である必要があります。

  • Cloud 請求先アカウントには、クレジット カードなどのお支払い方法が必要です。
  • Firebase と Google Cloud を初めて使用する場合は、$300 のクレジットと無料トライアル用 Cloud 請求先アカウントを利用できるかどうか確認してください。
  • この Codelab をイベントの一環として実施する場合は、利用可能な Cloud クレジットがあるかどうかを主催者に確認してください。

プロジェクトを Blaze プランにアップグレードする手順は次のとおりです。

  1. Firebase コンソールで、プランをアップグレードを選択します。
  2. Blaze プランを選択します。画面上の手順に沿って、Cloud 請求先アカウントをプロジェクトにリンクします。
    このアップグレードの一環として Cloud 請求先アカウントを作成する必要があった場合は、Firebase コンソールのアップグレード フローに移動してアップグレードを完了する必要があります。

Firebase プロジェクトにウェブアプリを追加する

  1. Firebase プロジェクトの [プロジェクトの概要] に移動し、e41f2efdd9539c31.png [ウェブ] をクリックします。

    プロジェクトにすでにアプリが登録されている場合は、[アプリを追加] をクリックしてウェブアイコンを表示します。
  2. [アプリのニックネーム] テキスト ボックスに、覚えやすいアプリのニックネーム(My Next.js app など)を入力します。
  3. [このアプリの Firebase Hosting も設定します] チェックボックスはオフのままにします。
  4. [アプリの登録] > [次へ] > [次へ] > [コンソールに進む] をクリックします。

Firebase コンソールで Firebase サービスを設定する

Authentication を設定する

  1. Firebase コンソールで [認証] に移動します。
  2. [Get started] をクリックします。
  3. [その他のプロバイダ] 列で、[Google] > [有効にする] をクリックします。
  4. [プロジェクトの公開名] テキスト ボックスに、覚えやすい名前(My Next.js app など)を入力します。
  5. [プロジェクトのサポートメール] プルダウンからメールアドレスを選択します。
  6. [保存] をクリックします。

Cloud Firestore を設定する

  1. Firebase コンソールの左側のパネルで [Build] を開き、[Firestore データベース] を選択します。
  2. [データベースを作成] をクリックします。
  3. [データベース ID] は (default) のままにします。
  4. データベースのロケーションを選択し、[次へ] をクリックします。
    実際のアプリの場合は、ユーザーに近いロケーションを選択します。
  5. [テストモードで開始] をクリックします。セキュリティ ルールに関する免責条項を確認します。
    この Codelab の後半で、セキュリティ ルールを追加してデータを保護します。データベースのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。
  6. [作成] をクリックします。

Cloud Storage for Firebase を設定する

  1. Firebase コンソールの左側のパネルで [Build] を開き、[Storage] を選択します。
  2. [開始] をクリックします。
  3. デフォルトの Storage バケットのロケーションを選択します。
    US-WEST1US-CENTRAL1US-EAST1 のバケットでは、Google Cloud Storage の「Always Free」階層を利用できます。他のすべてのロケーションのバケットは、Google Cloud Storage の料金と使用量に従います。
  4. [テストモードで開始] をクリックします。セキュリティ ルールに関する免責条項を確認します。
    この Codelab の後半で、セキュリティ ルールを追加してデータを保護します。Storage バケットのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。
  5. [作成] をクリックします。

4. スターター コードベースを確認する

このセクションでは、この Codelab で機能を追加するアプリのスターター コードベースのいくつかの領域を確認します。

フォルダとファイルの構造

次の表に、アプリのフォルダとファイル構造の概要を示します。

フォルダとファイル

説明

src/components

フィルタ、ヘッダー、レストランの詳細、レビュー用の React コンポーネント

src/lib

React や Next.js にバインドされていないユーティリティ関数

src/lib/firebase

Firebase 固有のコードと Firebase 構成

public

ウェブアプリの静的アセット(アイコンなど)

src/app

Next.js App Router を使用したルーティング

src/app/restaurant

API ルート ハンドラ

package.jsonpackage-lock.json

npm を使用したプロジェクトの依存関係

next.config.js

Next.js 固有の構成(サーバー アクションが有効

jsconfig.json

JavaScript 言語サービスの構成

サーバー コンポーネントとクライアント コンポーネント

このアプリは、App ルーターを使用する Next.js ウェブアプリです。サーバー レンダリングはアプリ全体で使用されます。たとえば、src/app/page.js ファイルはメインページを担当するサーバー コンポーネントです。src/components/RestaurantListings.jsx ファイルは、ファイルの先頭にある "use client" ディレクティブで表されるクライアント コンポーネントです。

インポート ステートメント

次のような import ステートメントが見つかります。

import RatingPicker from "@/src/components/RatingPicker.jsx";

このアプリでは、パスエイリアスによって、@ 記号を使用して、かさばる相対インポートパスを回避しています。

Firebase 固有の API

すべての Firebase API コードは src/lib/firebase ディレクトリにラップされています。個々の React コンポーネントは、Firebase 関数を直接インポートするのではなく、src/lib/firebase ディレクトリからラップされた関数をインポートします。

モックデータ

レストランとレビューのモックデータは src/lib/randomData.js ファイルに含まれています。このファイルのデータは、src/lib/fakeRestaurants.js ファイルのコードに組み込まれます。

5. App Hosting バックエンドを作成する

このセクションでは、Git リポジトリのブランチを監視する App Hosting バックエンドを設定します。

このセクションの終わりまでに、GitHub のリポジトリに接続された App Hosting バックエンドが作成されます。このバックエンドは、main ブランチに新しい commit を push するたびに、アプリの新しいバージョンを自動的に再ビルドしてロールアウトします。

セキュリティ ルールをデプロイする

このコードには、Firestore と Cloud Storage for Firebase のセキュリティ ルールのセットがすでに含まれています。セキュリティ ルールをデプロイすると、データベースとバケット内のデータを不正使用から保護できます。

  1. ターミナルで、前に作成した Firebase プロジェクトを使用するように CLI を構成します。
    firebase use --add
    
    エイリアスを求められたら、friendlyeats-codelab と入力します。
  2. これらのセキュリティ ルールをデプロイするには、ターミナルで次のコマンドを実行します。
    firebase deploy --only firestore:rules,storage
    
  3. "Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?"」というメッセージが表示されたら、Enter を押して [はい] を選択します。

Firebase 構成をウェブアプリのコードに追加する

  1. Firebase コンソールで プロジェクトの設定に移動します。
  2. [SDK の設定と構成] ペインで [アプリを追加] をクリックし、コードの角かっこアイコン をクリックして、新しいウェブアプリを登録します。
  3. ウェブアプリ作成フローの最後に、firebaseConfig 変数とそのプロパティと値をコピーします。
  4. コードエディタで apphosting.yaml ファイルを開き、環境変数の値に Firebase コンソールの構成値を入力します。
  5. ファイルで、既存のプロパティをコピーしたプロパティに置き換えます。
  6. ファイルを保存します。

バックエンドの作成

  1. Firebase コンソールの [App Hosting] ページに移動します。

App Hosting コンソールのゼロ状態([使ってみる] ボタンあり)

  1. [使ってみる] をクリックして、バックエンドの作成フローを開始します。バックエンドを次のように構成します。
  2. 最初の手順のメッセージに沿って、先ほど作成した GitHub リポジトリを接続します。
  3. デプロイ設定を設定します。
    1. ルート ディレクトリを / のままにする
    2. ライブブランチを main に設定します。
    3. 自動ロールアウトを有効にする
  4. バックエンドに friendlyeats-codelab という名前を付けます。
  5. [Firebase ウェブアプリを作成または関連付ける] で、[既存の Firebase ウェブアプリを選択する] プルダウンから、前に構成したウェブアプリを選択します。
  6. [完了してデプロイ] をクリックします。しばらくすると、新しいページが表示され、新しい App Hosting バックエンドのステータスを確認できます。
  7. ロールアウトが完了したら、[ドメイン] で無料ドメインをクリックします。DNS の伝播により、動作を開始するまでに数分かかることがあります。

これで、最初のウェブアプリがデプロイされました。GitHub リポジトリの main ブランチに新しい commit を push するたびに、Firebase コンソールで新しいビルドとロールアウトが開始されます。ロールアウトが完了すると、サイトが自動的に更新されます。

6. ウェブアプリに認証を追加する

このセクションでは、ウェブアプリに認証を追加して、ウェブアプリにログインできるようにします。

ログインとログアウトの関数を実装する

  1. src/lib/firebase/auth.js ファイルで、onAuthStateChangedsignInWithGooglesignOut 関数を次のコードに置き換えます。
export function onAuthStateChanged(cb) {
	return _onAuthStateChanged(auth, cb);
}

export async function signInWithGoogle() {
  const provider = new GoogleAuthProvider();

  try {
    await signInWithPopup(auth, provider);
  } catch (error) {
    console.error("Error signing in with Google", error);
  }
}

export async function signOut() {
  try {
    return auth.signOut();
  } catch (error) {
    console.error("Error signing out with Google", error);
  }
}

このコードでは、次の Firebase API を使用します。

Firebase API

説明

GoogleAuthProvider

Google 認証プロバイダ インスタンスを作成します。

signInWithPopup

ダイアログベースの認証フローを開始します。

auth.signOut

ユーザーをログアウトします。

src/components/Header.jsx ファイルでは、コードですでに signInWithGoogle 関数と signOut 関数が呼び出されています。

  1. commit メッセージ「Adding Google Authentication」で commit を作成し、GitHub リポジトリに push します。1. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  2. ウェブアプリでページを更新し、[Google でログイン] をクリックします。ウェブアプリが更新されないため、ログインが成功したかどうか不明です。

認証状態をサーバーに送信する

認証状態をサーバーに渡すために、サービス ワーカーを使用します。fetchWithFirebaseHeaders 関数と getAuthIdToken 関数を次のコードに置き換えます。

async function fetchWithFirebaseHeaders(request) {
  const app = initializeApp(firebaseConfig);
  const auth = getAuth(app);
  const installations = getInstallations(app);
  const headers = new Headers(request.headers);
  const [authIdToken, installationToken] = await Promise.all([
    getAuthIdToken(auth),
    getToken(installations),
  ]);
  headers.append("Firebase-Instance-ID-Token", installationToken);
  if (authIdToken) headers.append("Authorization", `Bearer ${authIdToken}`);
  const newRequest = new Request(request, { headers });
  return await fetch(newRequest);
}

async function getAuthIdToken(auth) {
  await auth.authStateReady();
  if (!auth.currentUser) return;
  return await getIdToken(auth.currentUser);
}

サーバーの認証状態を読み取る

FirebaseServerApp を使用して、クライアントの認証状態をサーバーにミラーリングします。

src/lib/firebase/serverApp.js を開き、getAuthenticatedAppForUser 関数を置き換えます。

export async function getAuthenticatedAppForUser() {
  const idToken = headers().get("Authorization")?.split("Bearer ")[1];
  console.log('firebaseConfig', JSON.stringify(firebaseConfig));
  const firebaseServerApp = initializeServerApp(
    firebaseConfig,
    idToken
      ? {
          authIdToken: idToken,
        }
      : {}
  );

  const auth = getAuth(firebaseServerApp);
  await auth.authStateReady();

  return { firebaseServerApp, currentUser: auth.currentUser };
}

認証の変更をサブスクライブする

認証の変更をサブスクライブする手順は次のとおりです。

  1. src/components/Header.jsx ファイルに移動する
  2. 関数全体を次の useUserSession に置き換えます。
function useUserSession(initialUser) {
	// The initialUser comes from the server via a server component
	const [user, setUser] = useState(initialUser);
	const router = useRouter();

	// Register the service worker that sends auth state back to server
	// The service worker is built with npm run build-service-worker
	useEffect(() => {
		if ("serviceWorker" in navigator) {
			const serializedFirebaseConfig = encodeURIComponent(JSON.stringify(firebaseConfig));
			const serviceWorkerUrl = `/auth-service-worker.js?firebaseConfig=${serializedFirebaseConfig}`
		
		  navigator.serviceWorker
			.register(serviceWorkerUrl)
			.then((registration) => console.log("scope is: ", registration.scope));
		}
	  }, []);

	useEffect(() => {
		const unsubscribe = onAuthStateChanged((authUser) => {
			setUser(authUser)
		})

		return () => unsubscribe()
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, []);

	useEffect(() => {
		onAuthStateChanged((authUser) => {
			if (user === undefined) return

			// refresh when user changed to ease testing
			if (user?.email !== authUser?.email) {
				router.refresh()
			}
		})
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [user])

	return user;
}

このコードでは、React の状態フックを使用して、onAuthStateChanged 関数で認証状態の変更が指定されたときにユーザーを更新します。

変更を確認する

src/app/layout.js ファイルのルート レイアウトはヘッダーをレンダリングし、ユーザーが利用可能な場合はプロップとして渡します。

<Header initialUser={currentUser?.toJSON()} />

つまり、<Header> コンポーネントは、サーバーの実行時にユーザーデータ(利用可能な場合)をレンダリングします。初回のページ読み込み後のページ ライフサイクル中に認証が更新された場合、onAuthStateChanged ハンドラがその更新を処理します。

次は、新しいビルドをロールアウトして、ビルド内容を確認します。

  1. commit メッセージ「Show signin state」で commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. 新しい認証動作を確認します。
    1. ブラウザでウェブアプリを更新します。表示名がヘッダーに表示されます。
    2. ログアウトしてもう一度ログインしてください。 ページの更新なしでページがリアルタイムで更新されます。この手順は、別のユーザーに対しても繰り返すことができます。
    3. 省略可: ウェブアプリを右クリックし、[ページのソースを表示] を選択して、表示名を検索します。サーバーから返された未加工の HTML ソースに表示されます。

7. レストラン情報を表示する

このウェブアプリには、レストランとレビューのモックデータが含まれています。

レストランを 1 つ以上追加する

ローカルの Cloud Firestore データベースにレストランのモックデータを挿入する手順は次のとおりです。

  1. ウェブアプリで、2cf67d488d8e6332.png > サンプルのレストランを追加を選択します。
  2. Firebase コンソールの [Firestore データベース] ページで、[レストラン] を選択します。レストラン コレクションには、それぞれがレストランを表す最上位のドキュメントが表示されます。
  3. いくつかのドキュメントをクリックして、レストランのドキュメントのプロパティを確認します。

レストランのリストを表示する

Cloud Firestore データベースに、Next.js ウェブアプリで表示できるレストランが追加されました。

データ取得コードを定義する手順は次のとおりです。

  1. src/app/page.js ファイルで <Home /> サーバー コンポーネントを見つけ、getRestaurants 関数の呼び出しを確認します。この関数は、サーバー実行時にレストランのリストを取得します。getRestaurants 関数を実装する手順は次のとおりです。
  2. src/lib/firebase/firestore.js ファイルで、applyQueryFilters 関数と getRestaurants 関数を次のコードに置き換えます。
function applyQueryFilters(q, { category, city, price, sort }) {
	if (category) {
		q = query(q, where("category", "==", category));
	}
	if (city) {
		q = query(q, where("city", "==", city));
	}
	if (price) {
		q = query(q, where("price", "==", price.length));
	}
	if (sort === "Rating" || !sort) {
		q = query(q, orderBy("avgRating", "desc"));
	} else if (sort === "Review") {
		q = query(q, orderBy("numRatings", "desc"));
	}
	return q;
}

export async function getRestaurants(db = db, filters = {}) {
	let q = query(collection(db, "restaurants"));

	q = applyQueryFilters(q, filters);
	const results = await getDocs(q);
	return results.docs.map(doc => {
		return {
			id: doc.id,
			...doc.data(),
			// Only plain objects can be passed to Client Components from Server Components
			timestamp: doc.data().timestamp.toDate(),
		};
	});
}
  1. 「Firestore からレストランのリストを読み取る」という commit メッセージで commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. ウェブアプリでページを更新します。レストランの画像がページ上にタイルとして表示されます。

レストランのリスティングがサーバー実行時に読み込まれることを確認する

Next.js フレームワークを使用すると、データがサーバー実行時とクライアントサイド実行時のどちらで読み込まれるかが明確でない場合があります。

レストランのリスティングがサーバー実行時に読み込まれることを確認する手順は次のとおりです。

  1. ウェブアプリで DevTools を開き、JavaScript を無効にします。

DevTools で JavaScipt を無効にする

  1. ウェブアプリを更新しても、レストランのリスティングは引き続き読み込まれます。レストラン情報はサーバー レスポンスで返されます。JavaScript が有効になっている場合、レストラン情報はクライアントサイドの JavaScript コードを介してハイドレートされます。
  2. DevTools で、JavaScript を再度有効にする

Cloud Firestore スナップショット リスナーでレストランの更新をリッスンする

前のセクションでは、src/app/page.js ファイルからレストランの初期セットが読み込まれる方法を確認しました。src/app/page.js ファイルはサーバー コンポーネントであり、Firebase データ取得コードを含むサーバー上でレンダリングされます。

src/components/RestaurantListings.jsx ファイルはクライアント コンポーネントであり、サーバー レンダリングされたマークアップをハイドレートするように構成できます。

サーバー側でレンダリングされたマークアップをハイドレートするように src/components/RestaurantListings.jsx ファイルを構成する手順は次のとおりです。

  1. src/components/RestaurantListings.jsx ファイルで、すでに記述されている次のコードを確認します。
useEffect(() => {
        const unsubscribe = getRestaurantsSnapshot(data => {
                setRestaurants(data);
        }, filters);

        return () => {
                unsubscribe();
        };
}, [filters]);

このコードは、前のステップで実装した getRestaurants() 関数に似ている getRestaurantsSnapshot() 関数を呼び出します。ただし、このスナップショット関数にはコールバック メカニズムが用意されているため、レストランのコレクションに変更が加えられるたびにコールバックが呼び出されます。

  1. src/lib/firebase/firestore.js ファイルで、getRestaurantsSnapshot() 関数を次のコードに置き換えます。
export function getRestaurantsSnapshot(cb, filters = {}) {
	if (typeof cb !== "function") {
		console.log("Error: The callback parameter is not a function");
		return;
	}

	let q = query(collection(db, "restaurants"));
	q = applyQueryFilters(q, filters);

	const unsubscribe = onSnapshot(q, querySnapshot => {
		const results = querySnapshot.docs.map(doc => {
			return {
				id: doc.id,
				...doc.data(),
				// Only plain objects can be passed to Client Components from Server Components
				timestamp: doc.data().timestamp.toDate(),
			};
		});

		cb(results);
	});

	return unsubscribe;
}

[Firestore データベース] ページで行った変更がウェブアプリにリアルタイムで反映されるようになりました。

  1. commit メッセージ「レストランのリアルタイム更新をリッスン」で commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. ウェブアプリで、27ca5d1e8ed8adfe.png > サンプルのレストランを追加を選択します。スナップショット関数が正しく実装されていると、ページを更新しなくてもレストランがリアルタイムで表示されます。

8. ウェブアプリからユーザーが送信したレビューを保存する

  1. src/lib/firebase/firestore.js ファイルで、updateWithRating() 関数を次のコードに置き換えます。
const updateWithRating = async (
	transaction,
	docRef,
	newRatingDocument,
	review
) => {
	const restaurant = await transaction.get(docRef);
	const data = restaurant.data();
	const newNumRatings = data?.numRatings ? data.numRatings + 1 : 1;
	const newSumRating = (data?.sumRating || 0) + Number(review.rating);
	const newAverage = newSumRating / newNumRatings;

	transaction.update(docRef, {
		numRatings: newNumRatings,
		sumRating: newSumRating,
		avgRating: newAverage,
	});

	transaction.set(newRatingDocument, {
		...review,
		timestamp: Timestamp.fromDate(new Date()),
	});
};

このコードは、新しいレビューを表す新しい Firestore ドキュメントを挿入します。また、レストランを表す既存の Firestore ドキュメントも更新され、評価の数と計算された平均評価の最新の値が反映されます。

  1. 関数全体を次の addReviewToRestaurant() に置き換えます。
export async function addReviewToRestaurant(db, restaurantId, review) {
	if (!restaurantId) {
		throw new Error("No restaurant ID has been provided.");
	}

	if (!review) {
		throw new Error("A valid review has not been provided.");
	}

	try {
		const docRef = doc(collection(db, "restaurants"), restaurantId);
		const newRatingDocument = doc(
			collection(db, `restaurants/${restaurantId}/ratings`)
		);

		// corrected line
		await runTransaction(db, transaction =>
			updateWithRating(transaction, docRef, newRatingDocument, review)
		);
	} catch (error) {
		console.error(
			"There was an error adding the rating to the restaurant",
			error
		);
		throw error;
	}
}

Next.js サーバー アクションを実装する

Next.js サーバー アクションには、フォームデータにアクセスするための便利な API が用意されています。たとえば、data.get("text") はフォーム送信ペイロードからテキスト値を取得します。

Next.js サーバー アクションを使用してレビュー フォームの送信を処理する手順は次のとおりです。

  1. src/components/ReviewDialog.jsx ファイルで、<form> 要素の action 属性を見つけます。
<form action={handleReviewFormSubmission}>

action 属性値は、次の手順で実装する関数を参照します。

  1. src/app/actions.js ファイルで、handleReviewFormSubmission() 関数を次のコードに置き換えます。
// This is a next.js server action, which is an alpha feature, so
// use with caution.
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
export async function handleReviewFormSubmission(data) {
        const { app } = await getAuthenticatedAppForUser();
        const db = getFirestore(app);

        await addReviewToRestaurant(db, data.get("restaurantId"), {
                text: data.get("text"),
                rating: data.get("rating"),

                // This came from a hidden form field.
                userId: data.get("userId"),
        });
}

レストランのクチコミを追加する

レビューの送信のサポートを実装したので、レビューが Cloud Firestore に正しく挿入されていることを確認できます。

レビューを追加して Cloud Firestore に挿入されたことを確認する手順は次のとおりです。

  1. commit メッセージ「ユーザーがレストランのレビューを送信できるようにする」で commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. ウェブアプリを更新し、ホームページからレストランを選択します。
  4. レストランのページで [3e19beef78bb0d0e.png] をクリックします。
  5. 評価を選択してください。
  6. クチコミを書く。
  7. [送信] をクリックします。レビューはレビューのリストの上部に表示されます。
  8. Cloud Firestore の [ドキュメントを追加] ペインで、レビューしたレストランのドキュメントを検索して選択します。
  9. [コレクションを開始] ペインで [評価] を選択します。
  10. [ドキュメントを追加] ペインで、確認するドキュメントを見つけて、想定どおりに挿入されていることを確認します。

Firestore エミュレータのドキュメント

9. ウェブアプリからユーザーがアップロードしたファイルを保存する

このセクションでは、ログイン時にレストランに関連付けられた画像を置き換えられるようにする機能を追加します。画像を Firebase Storage にアップロードし、レストランを表す Cloud Firestore ドキュメントで画像の URL を更新します。

ユーザーがアップロードしたファイルをウェブアプリから保存する手順は次のとおりです。

  1. src/components/Restaurant.jsx ファイルで、ユーザーがファイルをアップロードしたときに実行されるコードを確認します。
async function handleRestaurantImage(target) {
        const image = target.files ? target.files[0] : null;
        if (!image) {
                return;
        }

        const imageURL = await updateRestaurantImage(id, image);
        setRestaurant({ ...restaurant, photo: imageURL });
}

変更は必要ありませんが、次の手順で updateRestaurantImage() 関数の動作を実装します。

  1. src/lib/firebase/storage.js ファイルで、updateRestaurantImage() 関数と uploadImage() 関数を次のコードに置き換えます。
export async function updateRestaurantImage(restaurantId, image) {
	try {
		if (!restaurantId)
			throw new Error("No restaurant ID has been provided.");

		if (!image || !image.name)
			throw new Error("A valid image has not been provided.");

		const publicImageUrl = await uploadImage(restaurantId, image);
		await updateRestaurantImageReference(restaurantId, publicImageUrl);

		return publicImageUrl;
	} catch (error) {
		console.error("Error processing request:", error);
	}
}

async function uploadImage(restaurantId, image) {
	const filePath = `images/${restaurantId}/${image.name}`;
	const newImageRef = ref(storage, filePath);
	await uploadBytesResumable(newImageRef, image);

	return await getDownloadURL(newImageRef);
}

updateRestaurantImageReference() 関数はすでに実装されています。この関数は、更新された画像 URL を使用して、Cloud Firestore 内の既存のレストラン ドキュメントを更新します。

画像アップロード機能を確認する

画像が想定どおりにアップロードされていることを確認する手順は次のとおりです。

  1. 「ユーザーが各レストランの写真を変更できるようにする」という commit メッセージを付けて commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. ウェブアプリでログインしていることを確認し、レストランを選択します。
  4. 7067eb41fea41ff0.png をクリックして、ファイルシステムから画像をアップロードします。イメージはローカル環境から Cloud Storage にアップロードされます。アップロードした画像はすぐに表示されます。
  5. Firebase の Cloud Storage に移動します。
  6. レストランを表すフォルダに移動します。アップロードした画像がフォルダに存在する。

6cf3f9e2303c931c.png

10. 生成 AI でレストランのレビューを要約する

このセクションでは、レビューの要約機能を追加します。これにより、ユーザーはすべてのレビューを読まなくても、レストランに対する他のユーザーの意見を簡単に把握できます。

Gemini API キーを Cloud Secret Manager に保存する

  1. Gemini API を使用するには、API キーが必要です。Google AI Studio でキーを作成します。
  2. App Hosting は Cloud Secret Manager と統合されているため、API キーなどの機密情報を安全に保存できます。
    1. ターミナルで次のコマンドを実行して、新しいシークレットを作成します。
    firebase apphosting:secrets:set gemini-api-key
    
    1. シークレット値の入力を求められた場合は、Google AI Studio から Gemini API キーをコピーして貼り付けます。
    2. 新しいシークレットを apphosting.yaml に追加するかどうかを確認するメッセージが表示されたら、Y と入力して承認します。

Gemini API キーが Cloud Secret Manager に安全に保存され、App Hosting バックエンドからアクセスできるようになりました。

クチコミの概要コンポーネントを実装する

  1. src/components/Reviews/ReviewSummary.jsx で、GeminiSummary 関数を次のコードに置き換えます。
    export async function GeminiSummary({ restaurantId }) {
        const { firebaseServerApp } = await getAuthenticatedAppForUser();
        const reviews = await getReviewsByRestaurantId(
            getFirestore(firebaseServerApp),
            restaurantId
        );
    
        const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
        const model = genAI.getGenerativeModel({ model: "gemini-pro"});
    
        const reviewSeparator = "@";
        const prompt = `
            Based on the following restaurant reviews, 
            where each review is separated by a '${reviewSeparator}' character, 
            create a one-sentence summary of what people think of the restaurant. 
    
            Here are the reviews: ${reviews.map(review => review.text).join(reviewSeparator)}
        `;
    
        try {
            const result = await model.generateContent(prompt);
            const response = await result.response;
            const text = response.text();
    
            return (
                <div className="restaurant__review_summary">
                    <p>{text}</p>
                    <p>✨ Summarized with Gemini</p>
                </div>
            );
        } catch (e) {
            console.error(e);
            return <p>Error contacting Gemini</p>;
        }
    }
    
  2. 「AI を使用してレビューを要約する」という commit メッセージを含む commit を作成し、GitHub リポジトリに push します。
  3. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  4. レストランのページを開きます。ページの上部には、ページ上のすべてのレビューの概要が 1 文で表示されます。
  5. 新しいクチコミを追加してページを更新します。概要が変更されたことを確認します。

11. まとめ

これで完了です。Firebase を使用して Next.js アプリに機能と機能を追加する方法について学びました。具体的には、以下を使用しました。

詳細