When using Firestore in Next.js, the initialization method is different between CSR and SSR.
At first
When using Firestore in Next.js, you can retrieve data in CSR or SSR. I think that the method of retrieval changes depending on the page, I get a little addicted by the wrong initialization method, so I will summarize it in the article.
What this article says
- If you want to interact with Firestore in both CSR and SSR, the initialization must be written separately, using sample code provided by Vercel.
* For convenience, it is written only as SSR, but it is the same for SSG.
For CSR
Initialization required for CSR
Log in to the console and open the screen where the API key is displayed from プロジェクトの設定 → 全般.
Paste the ↑ information into the .env.
NEXT_PUBLIC_FIREBASE_API_KEY="***************"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="***************"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="***************"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="***************"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="***************"
NEXT_PUBLIC_FIREBASE_APP_ID="***************"
And ↓ Initialize it like this.
import firebase from 'firebase/app';
import 'firebase/firestore';
const credentials = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
if (!firebase.apps.length) {
firebase.initializeApp(credentials);
}
export default firebase;
That's it. After that, import it where you need it and use it.
One thing to note is that environment variable prefixes must be NEXT_PUBLIC_FIREBASE_.
For SSR
If you write it first, if you do not think about security rules at all, it will work even with the initialization described above. But in practice, writing security rules should be mandatory.
For example, suppose you set the following security rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
These are the orthodox rules that are also found in the Firebase documentation. Only authenticated users can access it.
It is a loose rule that authenticated users can access OK, even anonymous users can access. It cannot be accessed by SSR at this rate. This is usually because the credentials are held by the client side.
Initialization required for SSR
Use firebase-admin. Because firebase-admin operates Firebase from a privileged environment, it can access Firestore from the server side regardless of the rules.
To use firebase-admin, first download the private key (json file) by clicking 新しい秘密鍵の生成 from the サービスアカウント of the プロジェクトの設定.
※ This file should never be leaked to the outside. The API key written above is a problem if you strengthen the rules where it leaks, but if this file leaks, it is an accident.
There are project_id, client_email, and private_key in the json file you downloaded, so add them to the .env.
# for client-side
NEXT_PUBLIC_FIREBASE_API_KEY="***************"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="***************"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="***************"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="***************"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="***************"
NEXT_PUBLIC_FIREBASE_APP_ID="***************"
# for firebase-admin
FIREBASE_PROJECT_ID="***************"
FIREBASE_CLIENT_EMAIL="***************"
FIREBASE_PRIVATE_KEY="***************"
* NEXT_PUBLIC_ is not required. This prefix is what you need to bundle with js on the browser side.
* Since the contents of NEXT_PUBLIC_FIREBASE_PROJECT_ID and FIREBASE_PROJECT_ID are the same, it is possible to divert the NEXT_PUBLIC_FIREBASE_PROJECT_ID as it is, but I have deliberately added it to make it easier to understand what each one needs.
.
Initialize as follows:
import * as admin from 'firebase-admin';
if (!admin.apps.length) {
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, '\
'),
}),
});
}
export default admin;
Now that we have two initialization files, After that, import and use CSR and SSR where they are needed.
Next.js sample code to interact with Firestore
I think that it is difficult to attach an image even if only the initialization method is arranged, Using the sample code for cooperation with Firebase prepared by Vercel, I would like to explain while actually giving errors.
This is a simpler arrangement of this sample code.
Register the data on the client side and display it on the screen in SSR. Specifically, the flow is as follows.
Anonymous authentication login at the time the page is opened
Enter a name and message, and click "Create data in Firestore" to register the data in Firestore by client-side processing.
When you click "Go to SSR Page", the data registered earlier is displayed on the screen in SSR.
↓ Here is what we actually deployed
Client-side processing
Firestore rules are OK for authenticated users, just like in the example above.
import Head from 'next/head';
import Link from 'next/link';
import { useState, useEffect } from 'react';
// ↓ CSR 用として初期化してあるものインポート
import firebase from '../firebase/clientApp';
export const Home = () => {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const data = { name, message };
useEffect(() => {
firebase.auth().onAuthStateChanged(async (user) => {
// 匿名ユーザーを作成する
if (!user) {
firebase.auth().signInAnonymously();
}
});
});
// Firestore にデータを登録する関数
const createData = async () => {
if (!name || !message) {
alert('名前とメッセージを入力してください');
return;
}
const db = firebase.firestore();
await db.collection('profile').doc(name).set(data);
alert('Firestoreにデータを作成できました!');
};
return (
<div className="container">
<Head>
<title>Next.js / Firestore</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1 className="title">Next.js / Firebase CSR</h1>
<p className="description">名前とメッセージを入力してください。</p>
<div className="labelBox">
<label>
名前:
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<label>
メッセージ:
<input value={message} onChange={(e) => setMessage(e.target.value)} />
</label>
</div>
<button onClick={createData}>Firestoreにデータを作成</button>
<Link href={`/profile/${data.name}`} passHref>
<a>Go to SSR Page</a>
</Link>
</main>
//これより下はただのスタイルのため省略...
One thing to note is to import the file initialized for the CSR. If you accidentally call firestore using admin initialized for SSR, you will get an error.
Let's do it in practice.
import Head from 'next/head';
import Link from 'next/link';
import { useState, useEffect } from 'react';
import firebase from '../firebase/clientApp';
// ↓ SSR 用に初期化した admin をインポート
import admin from '../firebase/nodeApp';
// Firestore にデータを登録する関数
const createData = async () => {
if (!name || !message) {
alert('名前とメッセージを入力してください');
return;
}
// db を admin から作成
const db = admin.firestore();
await db.collection('profile').doc(name).set(data);
alert('Firestoreにデータを作成できました!');
};
When you start the app in this state,
Module not found: Can't resolve 'fs' error,
I get an error like Module not found: Can't resolve 'child_process'.
This is due to the fact that node.js-specific code is being read by the browser.
Server-side processing
Next, we will write the processing of the SSR.
// admin をインポート
import admin from '../firebase/nodeApp';
export const getProfileData = async (name) => {
// admin から db を作成
const db = admin.firestore();
const profileCollection = db.collection('profile');
const profileDoc = await profileCollection.doc(name).get();
if (!profileDoc.exists) {
return null;
}
// 取得したデータを返す
return profileDoc.data();
};
import Head from 'next/head';
// ↓ 上に書いた関数をインポート
import { getProfileData } from '../../fetchData/getProfileData';
const SSRPage = ({ data }) => {
const { name, profile } = data;
return (
<div className="container">
<Head>
<title>Next.js / Firestore</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1 className="title">Next.js / Firebase SSR</h1>
<h2>{name}</h2>
<p>{profile.message}</p>
</main>
</div>
);
};
export const getServerSideProps = async ({ params }) => {
const { name } = params;
// getServerSideProps 内で getProfileData() を実行する
const profile = await getProfileData(name);
if (!profile) {
return { notFound: true };
}
return { props: { data: { name, profile } } };
};
export default SSRPage;
One thing to note is to import the admin initialized for SSR. If you accidentally import the one initialized for CSR, you will get an error.
Let's actually do this as well.
// CSR 用に初期化した方をインポートします
import firebase from '../firebase/clientApp';
export const getProfileData = async (name) => {
// ここも admin から firebase に書き換えます。
const db = firebase.firestore();
const profileCollection = db.collection('profile');
const profileDoc = await profileCollection.doc(name).get();
if (!profileDoc.exists) {
return null;
}
return profileDoc.data();
};
Try launching the app in this state.
You can register data without any problem, but when you click the SSR page
I get an error saying Missing or insufficient permissions..
This is an error that appears when you get caught in a Firestore rule. Anonymous authentication is done on the client side, but it is not done on the server side, so it will be bounced.
Let's try to loosen the rules of Firestore.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
I made it accessible to everyone.
Now try running the app again.
I was able to load it.
If you don't want to expose it to the world, this method will be fine.
Summary
That's it for the difference in initialization methods.
As this sample code is also indicated,
I thought it was important to separate the initialization files and manage them in an easy-to-understand manner.
Tatsunori Nakano
2017年にWeb制作の現場に入り、2020年にReactでのWeb開発に進み、2021年からReact Nativeでのアプリ開発をしています。業務効率化のためにPythonを触ったりもします。
Updated on June 13, 2021
