Firebaseを用いたフロントエンド開発において、データの保存先として最も選ばれているのが「Cloud Firestore」です。
本記事では、Next.js(App Router/Pages Router)での実装を想定し、Firestoreの基本構造である「コレクション・ドキュメント・フィールド」の概念から、addDocやcollection関数を使った具体的なデータ保存の実装方法まで、実例付きで解説しています。
コレクション、ドキュメント、フィールド
Firestoreを使う上で「コレクション」と「ドキュメント」という用語は外せません。
SQLのテーブルに該当するのが「コレクション」、レコードに該当するのが「ドキュメント」です。
例えば、SQLで「users」というテーブルに、各ユーザーのデータ(レコード)を保存する場合、これをFirestoreに置き換えると「users」が「コレクション」、その中の各データ行が「ドキュメント」となります。
各ドキュメントの中には、キーと値で情報を保存していきます。この各キーと値のセットを「フィールド」と言います。
▼Firestoreのデータ保存の例

FirestoreでDBを作成する
Firebaseのアプリにログインし、左メニューの「構築 > Firestore Database」へと進みます。

「データベースの作成」を選択します。

Standardエディションを選びます。

ロケーションで「Tokyo」を選びます。

「テストモードで開始する」を選び「作成」をクリックします。

データベースに「default」が追加されればOKです。

プロジェクトの生成
next.jsでテスト用のプロジェクトを作成します。
npx create-next-app@latest test-firestorenext.jsでプロジェクトを作成する方法は下記をご参考ください。
> Next.jsとは何か?超簡単にWEBサイトを公開する方法を実例で解説|create-next-appツールの使い方
Success!と表示されれば完了です。

firebaseのインストール
firebaseを使うために必要なパッケージを作成します。
cd test-firestore
npm install firebasenpm install firebaseは、FirebaseのSDKをインストールするコマンドです。
SDKとは、開発に必要なnpmパッケージ、CLIツール、管理画面などが含まれたキットです。
npm install firebaseで変化があるファイルは「node_modukes」「package-lock.json 」「package.json」のみです。新たにフォルダやファイルが生成されるわけではありません。
Firebaseを動かすために必要なパッケージをインストールするのみです。
SDKの初期化設定(.env.localとfirebase.js)
Firebaseを動かすためのキット(SDK)は入りましたが、Googleのサーバーと接続するための認証情報は自分で作成しなければいけません。
そこでルートディレクトリに以下のフォルダとファイルを作成します。lib/firebase.js
mkdir lib
ni lib/firebase.jslibは、libraryの略で慣習的にこの中に「firebase.js」という設定ファイルを設置しAPIキーなどの重要な接続情報(環境設定)を記述していきます。
ですが、一般公開するとそれらの重要な情報をみんなが見れる状態になってしまいます。
そこで .env.localというファイルを作成し、その中に重要な情報を記述します。
.env.localとは何か?やfirebase.jsについては下記をご参考ください。
> .envと.env.localとは何か?違いやどちらを使うべきか?環境変数の優先順位や書き方・使い方を実例で分かりやすく解説
> 【Next.js】firebase.jsやNEXT_PUBLIC_, process.env.とは何か?.env.localの環境変数を読み込む方法
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSy...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-app.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-app-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-app.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789
NEXT_PUBLIC_FIREBASE_APP_ID=1:12345:web:abc123※.gitignoreに.env.localが記載されている(アップロード対象外になっている)ことを確認してください。
import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore"; //Firestoreの読み込み
import { getAuth } from "firebase/auth"; // Authも使う場合
//process.envで環境変数を読み込む
const firebaseConfig = {
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,
};
// Next.jsは開発中に何度もリロードされるため、二重初期化を防ぐこの書き方が推奨されます
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
const db = getFirestore(app);
const auth = getAuth(app);
export { app, db, auth };一番最後のexportで app, db, authを使えるようにしています。appを除外しても問題ありません。(authも不要な場合は除外して問題ありません)
export { db, auth };
appは、Firebase Storageを使う場合const storage = getStorage(app);や、Analyticsを使う場合 getAnalytics(app); に必要となります。
トップページの作成
app/pages.jsに以下を記述します。
"use client";
import { useState } from "react";
import { db } from "@/lib/firebase";
import { collection, addDoc, serverTimestamp } from "firebase/firestore";
export default function Home() {
// 入力値を管理するための「ステート」
const [formData, setFormData] = useState({ name: "", email: "" });
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState("");
const handleSave = async (e) => {
e.preventDefault(); // フォーム送信時のページリロードを防ぐ
if (!formData.name || !formData.email) {
setStatus("⚠️ 名前とメールアドレスを入力してください");
return;
}
setLoading(true);
setStatus("送信中...");
try {
// 入力された formData をそのまま保存
await addDoc(collection(db, "users"), {
name: formData.name,
email: formData.email,
createdAt: serverTimestamp(),
});
setStatus("✅ 保存に成功しました!");
setFormData({ name: "", email: "" }); // 送信後にフォームを空にする
} catch (e) {
console.error(e);
setStatus("❌ 保存に失敗しました");
} finally {
setLoading(false);
}
};
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-6">
<div className="w-full max-w-md overflow-hidden rounded-2xl bg-white shadow-xl">
<div className="bg-blue-600 p-6 text-center text-white">
<h1 className="text-2xl font-bold">ユーザー登録</h1>
<p className="mt-2 opacity-90">Firestoreへ保存します</p>
</div>
<form onSubmit={handleSave} className="p-8 space-y-4">
{/* 名前入力 */}
<div>
<label className="block text-sm font-medium text-gray-700">お名前</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none text-black"
placeholder="山田 太郎"
/>
</div>
{/* メール入力 */}
<div>
<label className="block text-sm font-medium text-gray-700">メールアドレス</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none text-black"
placeholder="example@test.com"
/>
</div>
<button
type="submit"
disabled={loading}
className={`w-full rounded-lg py-3 font-semibold text-white transition-all ${
loading ? "bg-gray-400" : "bg-blue-600 hover:bg-blue-700"
}`}
>
{loading ? "送信中..." : "データベースに保存"}
</button>
{status && (
<div className={`mt-4 rounded p-3 text-center text-sm ${
status.includes("成功") ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"
}`}>
{status}
</div>
)}
</form>
</div>
</main>
);
}上記の例では、CSSにTailwindを使っています。
Next.jsでnpx create-next-app@latestを実行したときに「Would you like to use Tailwind CSS?」で Yes と答えていれば、上記の記述が可能になります。

「データベースに保存」をクリックすると、入力データをFirestorageに保存します。

Firestoreのデータベースを確認すると指定したコレクション(users)に対象のドキュメントが保存されていることがわかります。

コード解説
use client(クライアントコンポーネントの宣言)
"use client";このファイルを「クライアントコンポーネント」として扱う宣言です。
Next.js(特にApp Router)では、デフォルトで「サーバー側」で動く仕組みになっています。しかし、以下の機能を使いたい場合は「ブラウザ(クライアント)側」で動かす必要があります。
- ユーザーが入力した文字を管理する (useState)
- ボタンを押したときの動き (onClick)
- ブラウザから直接Firebaseと通信する
この1行を先頭に書くことで、「このファイルはブラウザで実行するよ!」とNext.jsに伝えています。
今回の場合、useStateもonClickもFirebaseとの通信も行うので、"use client";の記述は必須です。use clientとは何かについては下記をご参考ください。
> 【Next.js/React.js】use clientとは何か?サーバーコンポーネントとクライアントコンポーネントの違い
useState(入力値や状態に応じた動的な表示切り替え)
import { useState } from "react";ReactのuseStateは、コンポーネントに「状態(ステート)」を持たせるための最も基本的なフック(Hook)です。
ユーザーの操作などで値が変わったときに、画面を自動的に書き換えたいデータを管理するために使います。
useStateを使うと、値が変わった場合に、画面を再描画(レンダリング)することができます。
ここでは、①ユーザーの入力内容の記憶と、②送信状態の切り替えのためにuseStateを使っています。
const [formData, setFormData] = useState({ name: "", email: "" });
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState("");
const handleSave = async (e) => {
e.preventDefault(); // フォーム送信時のページリロードを防ぐ
if (!formData.name || !formData.email) {
setStatus("⚠️ 名前とメールアドレスを入力してください");
return;
}
setLoading(true);
setStatus("送信中...");firestoreの使用
import { db } from "@/lib/firebase";
import { collection, addDoc, serverTimestamp } from "firebase/firestore";1行目で、firebase.jsの内容を読み込み、その中からデータベースの情報を変数名dbとしてインポートしています。
@でローカルフォルダを指定しています。(@がどのパスに該当するかは、tsconfig.jsonファイルの中に記述があります)
2行目は、node_modulesの中のfirebaseのパッケージから、firestoreの操作に必要な変数を呼び出しています。
firestoreへのデータ保存
firestoreへのデータ保存は以下のコードが担います。
await addDoc(collection(db, "users"), {
name: formData.name,
email: formData.email,
createdAt: serverTimestamp(),
});addDoc は、Firestoreのコレクションに対して「ドキュメントIDを自動的に生成して、新しいデータを追加する」ためのメソッドです。
「どのコレクションに(参照)」、「何を(データ)」入れるかを指定します。
const docRef = await addDoc(collection(db, "コレクション名"), {
フィールド名: 値
});serverTimestamp() と new Date()
firestoreのモジュールから「serverTimestamp」というメソッドを呼び出しています。そして、データ保存時のタイムスタンプとして使用しています。
タイムスタンプとしてよく使われるものに「new Date()」があります。
ここではなぜ「new Date()」ではなく「serverTimestamp()」を使ったのかには明確な理由があります。
「new Date()」は実行した人のデバイスの現在時刻を読み込むものです。このため、その人のPCの時刻が狂っていれば時間もおかしくなります。
「serverTimestamp()」はGoogleのFirebaseのサーバーの時刻を取得するものです。これであればPCの設定に寄らず、一律のルールに従った時間を設定することができます。


