この記事では、
- Next.jsの動的ルーティングのパスにFirestoreにあるコレクションのドキュメントIDを使うための実装方法
- Next.jsのgetStaticPathsの中でFirestoreに格納されている値を使うために、Firebaseにサーバーサイドからアクセスする方法
- Firebaseにクライアントサイドとサーバーサイドからアクセスする場合の違い
などについて解説していきます。
例えば、複数の会社が使うシステムを開発する際に、ログイン画面をそのシステムを利用する会社ごとに分ける必要があるとします。
https://xxx.com/abc-company/signin
https://xxx.com/def-company/signin
という感じですね。
Next.jsの動的ルーティングという機能を使うことで、上記のようなことを実装できます。
動的ルーティングのパスにFirestoreに格納されているドキュメントIDを使う場合、以下の2つのやり方があります。
-
Firebaseにサーバーサイドからアクセスし、Next.jsではgetStaticPathsで指定するパスにFirestoreから取得した値を設定する
-
Next.jsでユーザーがアクセスしたURLのパスを取得し、そのURLのパスがIDになっているドキュメントががFirestoreに存在するかどうかを確認し、存在する場合と存在しない場合で表示する内容を分ける
前者はサーバーサイドからFirebaseにアクセスするのに対し、後者はクライアントサイドからFirebaseにアクセスするという違いがあり、Firebaseにサーバーサイドからアクセスする方法とクライアントサイドからアクセスする方法には大きな違いがあります。
これら2つの実装のやり方と、それぞれどのような違いがあるかについてもこの記事で解説していきます。
開発環境
- macOS Catalina 10.15.7
- Next.js 13.1.1
- Firebase 9.16.0
- firebase-admin 11.4.1
※Next.jsはJavaScriptを使っています。
クライアントサイドでルーティングを管理する場合とサーバーサイドでルーティングを管理する場合の違い
Next.jsの動的ルーティングをサーバーサイドで管理する場合とクライアントサイドで管理する場合の違いは以下のようになります。
-
サーバーサイドでFirestoreにアクセスし、動的ルーティングのパスとして使うコレクションから全てのドキュメントのIDを取得する
-
動的ルーティングを行うページのgetStaticPathsに取得したドキュメントIDを渡す
-
動的なルーティングを行うページがプリレンダリングされる
-
ユーザーが動的ルーティングのページにアクセスする
-
Firestoreから取得したドキュメントのIDにユーザーがアクセスしたパスと同じもがあればページコンポーネントがレンダリングされ、同じものがなかった場合は404のエラーページが出力される
-
動的なルーティングを行うページがプリレンダリングされる
-
ユーザーが動的ルーティングのページにアクセスする
-
ユーザーが開いたページのURLを取得して、そのURLの文字列がFirestoreに格納されているものかどうかを調べるためのリクエストを送る
-
ユーザーが開いたページのパスがFirestoreに格納されているドキュメントのIDになっていたかどうかの結果を返す
-
URLがFirestoreに格納されている文字列であれば、通常のページコンポーネントをレンダリングし、格納されていない文字列だった場合には「ページが存在しません」などのメッセージを表示する
サーバーサイドでもクライアントサイドでも動的ルーティングのパスを管理することはできます。
しかし、getStaticPathsでFirebaseのドキュメントIDを動的ルーティングのパスとして指定してサーバーサイドでルーティングを管理する場合、パスとしてFirestoreのドキュメントを全て取得しなければいけないので、クライアントサイドで管理するやり方の方が良いと思います。
では、ここから実際にサーバーサイドとクライアントサイド、それぞれからFirebaseにアクセスし、動的ルーティングのパスにFirestoreのドキュメントIDを使う方法を解説していきます。
Firestoreにコレクションとドキュメントを作成
まずは動的ルーティングのパスに使用するためのデータをFirestoreに作成します。
Firebaseのコンソールにアクセスし、プロジェクトのページを開いてサイドバーにあるFirestore Databaseを選択します。
ローケーションはTokyoを選択して、有効にするをクリックします。
ここからFirestoreのコレクションとドキュメントを作成していきます。「コレクションを開始」をクリックしてください。
今回のコレクションIDはtestとしておきます。コレクションIDを入力したら次へをクリックします。
自動IDをクリックするとドキュメントIDにランダムな文字列が入力されます(ドキュメントIDには好きな文字列を入れていただいても構いません)。保存を押すとドキュメントが保存されます。
このドキュメントIDがNext.jsの動的ルーティングのパスになるように実装していきます。このドキュメントIDが、先ほどの[uid]に入るパスの値になります。
他にも同じコレクションでドキュメントを作成したい場合は「ドキュメントを追加」で追加していくことができます。
サーバーサイドからFirebaseにアクセスして、動的ルーティングのパスにFiresotreから取得した値を使う方法
まずはサーバーサイドからFirebaseにアクセスし、Firestoreに格納されている値を取得してNext.jsの動的ルーティングのパスに使う方法を解説します。
動的ルーティングで表示させるページのファイルを作成
動的ルーティングで表示させるページをレンダリングするためのファイルを作成します。Next.jsのプロジェクトのpagesディレクトリに[uid]フォルダを作成し、その中にtest.jsファイルを作成します。
test.jsの中身は以下のように書きます。
const test = () => {
return (
<div>
テストページ
</div>
);
}
export default test;
ローカル環境でNext.jsのプロジェクトを起動しhttp://localhost:3000/aaa/test などの適当なURLにアクセスして、このテストページが問題なく表示されるかどうかを確認してみてください。
※上記の画面のスクショでは_app.jsのglobals.cssを読み込む記述を消しています。
firebase-adminをインストール
ターミナルで以下のコマンドを入力し、Next.jsのプロジェクトにfirebase-adminをインストールします。
npm i firebase-admin
このfirebase-adminを使うことによって、サーバーサイドからもFiresbaseにアクセスすることができます。
Firebaseから秘密鍵をダウンロード
Firebaseのプロジェクトの画面を開き、プロジェクト概要の左側にある歯車のマークから「プロジェクトの設定」を選択します。
プロジェクトの設定の画面に移動したらサービスアカウトをクリックします。すると以下のような画面になるので、新しい秘密鍵の生成をクリックしてファイルをダウンロードします。
※ダウンロードした秘密鍵は外部に公開しないように注意してください。
.envファイルに環境変数を追加
ダウンロードしたファイルの
- project_id
- client_email
- private_key
を環境変数として.envファイルに追加します(まだ.envファイルを作成していない場合はルートディレクトリに.envファイルを作成して環境変数を設定してください)。
FIREBASE_PROJECT_ID=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
FIREBASE_CLIENT_EMAIL=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
FIREBASE_PRIVATE_KEY=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
firebase-adminでデータを取得するためのファイルを作成
ルートディレクトリに/libフォルダを作成し、その中にdb.jsファイルを作成します。このファイルにサーバーサイドからFirestoreにアクセスし、ドキュメントIDを取得するための処理を書きます。
import * as admin from 'firebase-admin';
// 今後のために今使わないものもインポートしておく
import { getFirestore, collection, query, where, getDocs, doc } from 'firebase-admin/firestore';
// サーバーサイドでのみinitializeAppが行われるようにadmin.apps.length === 0としておく
if (admin.apps.length === 0) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
}),
});
}
// FirestoreのコレクションのドキュメントIDを取得する関数
export const getFirestorePaths = async () => {
const db = getFirestore();
// Firestoreからテストというコレクションを取得する
const test = db.collection('test');
// ドキュメントIDを格納するための配列を用意する
let paths = [];
// コレクションのスナップショットを取得する
const snapshot = await test.get();
// スナップショットから値を取り出し配列に格納する
snapshot.forEach(doc => {
// getStaticPathsの中でパスとして使える形でドキュメントIDを配列に入れる
paths.push({ params: { uid: doc.id } });
});
return paths;
}
このドキュメントIDはgetStaticPathsでパスとして使う値なので、配列に格納するときに、パスとして使える形式に整えています。
データを取得するための関数をgetStaticPathsの中で呼び出す
先ほどdb.jsに作成したFirestoreからデータを取得するための関数をtest.jsにインポートします。
そして、その関数をgetStaticPathsの中で呼び出し、パスの部分に渡します。
// db.jsからgetFirestorePaths関数をインポート
import { getFirestorePaths } from '../../lib/db';
const test = () => {
return (
<div>
テストページ
</div>
);
}
export default test;
// getStaticPathsでレンダリングするページのパスを指定する
export async function getStaticPaths() {
// レンダリングするページのパス
const paths = await getFirestorePaths();
return {
paths: paths,
// 定義されていないパス以外は404ページとして返したいのでfallback: false
fallback: false,
}
}
// getStaticPathsを使う場合にgetStaticPropsがないとエラーになるので書いておく
export async function getStaticProps() {
return {
props: { query: {} },
}
}
これでFirestoreのコレクションのドキュメントIDを、動的ルーティングのパスとして設定できました。
実際にパスとして設定されているURLにアクセスしてページが問題なく表示されることと、パスとして設定されていないURLにアクセスしたときに404のエラーページが表示されることを確認してください。
クライアントサイドからFirebaseにアクセスして、動的ルーティングのパスにFiresotreから取得した値を使う方法
続いては、クライアントサイドからFirebaseにアクセスし、Firestoreに格納されている値を取得してNext.jsの動的ルーティングのパスに使う方法を解説します。
Firebaseのインストール
Next.jsのプロジェクトにFirebaseのインストールを行います。
npm install firebase @types/firebase
npm install firebase
.envファイルに環境変数の設定
Firebaseのプロジェクト管理画面を開きます。
サイドバーのプロジェクト概要の右にある歯車のマークをクリックして、「プロジェクトの設定」を選択します。
画面を下にスクロールしていくと、firebaseConfigの値が表示されるので、これらの値を.envファイルに環境変数として設定していきます。
※まだ.envファイルを作成していない場合はプロジェクトのルートディレクトリに新しく.envファイルを作成してfirebaseConfigの値を環境変数として設定してください。
NEXT_PUBLIC_APIKEY=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
NEXT_PUBLIC_AUTHDOMAIN=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
NEXT_PUBLIC_PROJECTID=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
NEXT_PUBLIC_STORAGEBUCKET=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
NEXT_PUBLIC_MESSAGINGSENDERID=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
NEXT_PUBLIC_APPID=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
Firebaseの初期化
Next.jsのプロジェクトのルートディレクトリに新しくlibディレクトリを作成しFirebaseConfig.jsというファイルを作成し、以下のように中身を書きます。
import { initializeApp, getApps, FirebaseApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// Firestoreはログインやユーザー登録の実装には使わないが、今後のことを考えて入れておく
import { getFirestore, Firestore } from 'firebase/firestore'
import {
getAuth,
Auth,
} from "firebase/auth";
// .envファイルで設定した環境変数をfirebaseConfigに入れる
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_APIKEY,
authDomain: process.env.NEXT_PUBLIC_AUTHDOMAIN,
projectId: process.env.NEXT_PUBLIC_PROJECTID,
storageBucket: process.env.NEXT_PUBLIC_STORAGEBUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_MESSAGINGSENDERID,
appId: process.env.NEXT_PUBLIC_APPID
};
let firebaseApp = FirebaseApp;
let auth = Auth;
let firestore = getFirestore;
// サーバーサイドでレンダリングするときにエラーが起きないようにするための記述
if (typeof window !== "undefined" && !getApps().length) {
firebaseApp = initializeApp(firebaseConfig);
auth = getAuth();
firestore = getFirestore();
}
export { firebaseApp, auth, firestore };
このFirebaseConfig.jsがFirebaseの初期化を行うためのファイルです。
動的ルーティングで表示させるページのファイルを作成
次に、動的ルーティングで表示させるページのファイルを作成します。
pagesディレクトリの中の[uid]ディレクトリの中にcs_test.jsファイルを作成してください。
※サーバーサイドからFirebaseにアクセスするための実装のところで、pagesの中に[uid]ディレクトリを作成していない場合はここで新しく作成してください。
cs_test.jsの中身は、一旦以下のようにしておきます。
const Test = () => {
return (
<div>
テスト
</div>
);
}
export default Test;
FirestoreのドキュメントIDにユーザーがアクセスしたページのパスがあった場合となかった場合に表示するコンポーネントを作成
まずNext.jsのプロジェクトのルートディレクトリにcomponentsディレクトリを新しく作成してください。
ユーザーが/[uid]/cs_test.jsのページにアクセスしたとき、ページのパスのuidの部分がFirestoreのドキュメントIDに存在する場合と存在しない場合で、表示するコンポーネントをわけるため
- PageExist.js
- NoPage.js
の2つのコンポーネントをcomponentsディレクトリの中に作成し、中身をそれぞれ以下のように書きます。
const PageExist = () => {
return (
<div>
ページが存在しています。
</div>
);
}
export default PageExist;
const NoPage = () => {
return (
<div>
ページが存在していません。
</div>
);
}
export default NoPage;
クライアントサイドからFirestoreのドキュメントIDを取得する処理を書く
先ほど作成したcs_test.jsに
- ユーザーがアクセスしたページのパスを取得する
- Firestoreのtestコレクションから、IDがユーザーがアクセスしたページのパスと同じになっているドキュメントを取得する
- ドキュメントが存在していた場合には、PageExist.jsをレンダリングする
- ドキュメントが存在しなかった場合には、NoPage.jsをレンダリングする
という処理を実装します。具体的にはcs_test.jsの中身を以下のように書き換えます。
import { useRouter } from "next/router";
import { firestore } from '../../lib/FirebaseConfig';
// 現時点で使わないものもあるが今後のことを考えて入れておく
import { collection, query, where, getDocs, getDoc, doc, docs, getFirestore, setDoc } from 'firebase/firestore';
import { useEffect, useState } from "react";
// PageExistとNoPageコンポーネントを読み込む
import NoPage from "../../components/NoPage";
import PageExist from "../../components/PageExist";
const Test = () => {
const [ pageComponent, setPageComponent ] = useState();
const router = useRouter();
// ユーザーがアクセスしたページのパスを取得してurlに格納
const url = router.query.uid;
// routerとurlに変化があった場合だけこのuseEffectが実行される
useEffect(() => {
// urlが取得できていない状態でgetFirestoreDocが実行されないようにする
if (!router.isReady) {
return
}
// getFirestoreDoc関数を実行
getFirestoreDoc(url)
.then(
(data) => {
// getFirestoreDoc関数でリターンされたコンポーネントをpageComponentにセットする
setPageComponent(data);
},
(err) => {
console.log(err);
}
);
}, [router, url]);
return (
// pageComponentにはgetFirestoreDoc関数でリターンされたコンポーネントがセットされている
<>
{ pageComponent }
</>
);
}
export default Test;
// クライアントサイドでFirestoreのドキュメントを取得する関数
export const getFirestoreDoc = async (url) => {
// Firestoreのtestコレクションからユーザーがアクセスしたページのパスと同じドキュメントのリファレンス
const docRef = doc(firestore, "test", url);
// 上記の条件でドキュメントを取得
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
// ドキュメントが存在する場合はPageExistコンポーネントを返す
return <PageExist />
} else {
// ドキュメントが存在しない場合はNoPageコンポーネントを返す
return <NoPage />
}
}
では、上記のコードで何をやっているのかを詳しく解説していきます。
今回実現したいことは、ユーザーがアクセスしたURLのパスがIDになっているドキュメントがFirestoreに存在するかどうかで、ユーザーに表示される内容が変わるようにするということです。
そのために、まずユーザーがアクセスしている動的ルーティングのページのパスを取得します。
実際にパスを取得している処理がこちらの部分の処理です。
const router = useRouter();
// ユーザーがアクセスしたページのパスを取得してurlに格納
const url = router.query.uid;
useRouterを使ってユーザーがアクセスしたURLのパスの取得します。
動的ルーティングのパスの動的に変わる部分は、routerオブジェクトの中のqueryオブジェクトに格納されているため、今回のようにuidの部分が動的に変わるようになっている場合はrouter.query.uidで取得できます。
次に、取得したパスの文字列がIDになっているドキュメントがFirestoreにあるかどうかを判別して、
-
ドキュメントがあればPageExistコンポーネントをリターンする
-
ドキュメントがなければNoPageコンポーネントをリターンする
という関数を作成します。この関数がこちらのgetFirestoreDocという関数です。
// クライアントサイドでFirestoreのドキュメントを取得する関数
export const getFirestoreDoc = async (url) => {
// Firestoreのtestコレクションからユーザーがアクセスしたページのパスと同じドキュメントのリファレンス
const docRef = doc(firestore, "test", url);
// 上記の条件でドキュメントを取得
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
// ドキュメントが存在する場合はPageExistコンポーネントを返す
return <PageExist />
} else {
// ドキュメントが存在しない場合はNoPageコンポーネントを返す
return <NoPage />
}
}
次に、このgetFirestoreDoc関数がユーザーがアクセスしたURLのパスが変わったときに実行されるようにuseEffectの中でこの関数を呼び出し、PageExistもしくはNoPageのコンポーネントをpageComponentに格納するための処理を書きます。
こちらの処理が以下のコードの部分です。
// routerとurlに変化があった場合だけこのuseEffectが実行される
useEffect(() => {
// urlが取得できていない状態でgetFirestoreDocが実行されないようにする
if (!router.isReady) {
return
}
// getFirestoreDoc関数を実行
getFirestoreDoc(url)
.then(
(data) => {
// getFirestoreDoc関数でリターンされたコンポーネントをpageComponentにセットする
setPageComponent(data);
},
(err) => {
console.log(err);
}
);
}, [router, url]);
PageExistとNoPageのコンポーネントをcs_test.jsのページコンポーネントのリターンの中に直接書くような実装の仕方ではなく、関数でリターンするようにしているのには理由があります。
例えば、getFirestoreDocの関数でドキュメントが存在するかどうかのtrue,falseをリターンするようにして、ページコンポーネントの中にPageExistとNoPageコンポーネントを入れて条件分岐の処理を書いて、どちらを表示させるかを決めるような書き方もできます。
しかし、このような実装の仕方だとユーザーがアクセスしたURLのパスがFirestoreのドキュメントIDにあったとしても、ページを開いたときに一瞬だけNoPageコンポーネントが表示されてしまいます。
Firebaseからドキュメントを取得する処理よりもページコンポーネントをレンダリングする処理の方が先に完了するため、このような現象が起こるのだと思われます。
そういったこともあり、ドキュメントの有無によってPageExistとNoPageという別のコンポーネントをリターンする関数を使ってユーザーに表示される内容を変更するようにしています。
まとめ
サーバーサイドからFirebaseにアクセスしてgetStaticPathsの中で有効なパスを設定するというやり方でも、Next.jsの動的ルーティングのパスにFirestoreに格納されているドキュメントIDを使うことができます。
ただこのやり方だと、Firestoreに格納されている全てのドキュメントのIDをgetStaticPathsで取得する必要があり、もしFirestoreのドキュメントが100万件あった場合でも、それらを全て取得することになります。
それに対して、ユーザーがアクセスしたページのパスをuseRouterで取得して、そのパスがIDになっているドキュメントがFirestoreにあるかどうかを判別して、ユーザーに表示させるコンポーネントを変えるというやり方なら、Firestoreにある全てのドキュメントを取得する必要がないので、こちらの方が良いやり方なのではないかと思います。
今回の実装のgithubのリポジトリはこちらです。
https://github.com/masakiwakabayashi/nextjs_paths_firestore