メインコンテンツまでスキップ

第12章:リアルタイム購読①(onSnapshotで“勝手に更新”)⚡👀

この章は、**「一覧を見てるだけで勝手に更新される」**を体に入れる回です😆✨ やることはシンプルで、前の章までの getDocs()onSnapshot() に置き換えるだけ!💪


この章でできるようになること 🎯

  • ✅ Firestoreの「リアルタイム購読(Listener)」の感覚がわかる
  • ✅ Reactで安全に onSnapshot() を貼れる(=解除できる)
  • ✅ 別タブで追加した瞬間、一覧が増える “魔法” を体験できる🪄
  • ✅ 「解除しないと何が起きるか」を説明できる

Polling vs Realtime

1) リアルタイム購読って何?🤔⚡

Firestoreのリアルタイムは、ざっくり言うとこう👇

  • **「データ変わったら、向こうから通知が飛んでくる」**📨

  • だから ポーリング(何秒ごとに取りに行くやつ)不要🙅‍♂️

  • しかも自分の書き込みは “即” 画面に反映される(ラグ補正)⚡

    • ローカルの更新が先に届いて、あとでサーバー確定が来る…みたいな動きになるよ🧠
    • それを見分けるための metadata.hasPendingWrites も用意されてる👍 (Firebase)

2) まずは「置き換え前」を確認しよう 👀🧩

第7章あたりの一覧がこんな感じだったはず👇(例)

  • getDocs(query(...))一回だけ取得
  • 画面はその結果で固定(更新されない)

ここを onSnapshot() に差し替えて、以降は 勝手に更新されるようにします⚡


3) 手を動かす:一覧を onSnapshot() に切り替える 🛠️⚛️

3-1. 型(ToDo)を用意 🧱

「id付きの表示用データ」を作るのがポイントです😊

export type Todo = {
id: string;
title: string;
done: boolean;
createdAt?: unknown; // Timestamp型は後でちゃんと揃える(今は雰囲気でOK)
updatedAt?: unknown;
tags?: string[];
};

React Lifecycle & Subscription

3-2. onSnapshot() で購読して state に入れる 🔁📥

例:TodoList.tsx(一覧コンポーネント)に直書き版です👇 (後の章で hooks 化してキレイにします✨)

import { useEffect, useState } from "react";
import { collection, onSnapshot, orderBy, query } from "firebase/firestore";
import { db } from "../lib/firebase"; // 既に作ってある想定
import type { Todo } from "../types/Todo";

export function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
// 🔎 一覧は「新しい順」に並べる(createdAt desc)
const q = query(collection(db, "todos"), orderBy("createdAt", "desc"));

// ⚡ 購読スタート
const unsubscribe = onSnapshot(
q,
(snapshot) => {
const next = snapshot.docs.map((d) => {
const data = d.data() as Omit<Todo, "id">;
return { id: d.id, ...data };
});

setTodos(next);
setLoading(false);
setError(null);
},
(err) => {
// 権限エラーや無効クエリなどで落ちることがある
setError(err.message);
setLoading(false);
}
);

// 🧯 超重要:コンポーネントが消える時に解除!
return () => unsubscribe();
}, []);

if (loading) return <div>読み込み中…⏳</div>;
if (error) return <div>エラーだよ😇:{error}</div>;
if (todos.length === 0) return <div>まだ0件だよ📝</div>;

return (
<ul>
{todos.map((t) => (
<li key={t.id}>
{t.done ? "✅" : "⬜"} {t.title}
</li>
))}
</ul>
);
}
  • onSnapshot()変更があるたびに コールバックを呼びます📣 (Firebase)
  • さらに エラー用コールバックも付けられます(付けるのおすすめ)🧯 (Firebase)
  • unsubscribe() を呼ぶと 購読が止まる(解除できる)🛑 (Firebase)

onSnapshot Logic

4) ミニ課題:別タブで「増える」体験しよう 🪄🧪

やること(3分)⏱️

  1. ブラウザで 同じアプリを2タブ開く🧑‍💻🧑‍💻
  2. 片方のタブで ToDo を追加 ➕
  3. もう片方のタブの一覧が 自動で増えるのを確認🎉

**見えたら勝ち!**😆⚡


Subscription Leak

5) ここが落とし穴:解除しないと何が起きる?💥🧠

onSnapshot()貼ったら貼りっぱなしになりがちです。

ありがち事故 😇

  • 画面遷移するたびに購読が増えて、同じ更新が複数回来る
  • メモリも通信も無駄に増える
  • そして「なんか重い…」になる🐢

だから React では、

  • useEffect(() => { ...; return () => unsubscribe(); }, [])

これが基本形です✨ (第13章で「hooks化&安全運用」をやるよ!)


Latency Compensation

6) “即反映”の正体:ローカル変更イベント 🏎️💨

Firestoreは、書き込みしたら サーバーに届く前でも リスナーをすぐ動かします⚡ これが ラグ補正(latency compensation) です。 (Firebase)

イベントが「ローカル由来」か「サーバー確定」かを見分けたいなら👇

  • doc.metadata.hasPendingWrites を使う

    • true → まだローカルの仮状態
    • false → サーバー確定✨ (Firebase)

7) 上級者っぽい小ワザ:メタデータ変化も取りたい時 🕵️‍♂️✨

デフォルトだと「データそのものが変わった時」だけ通知されます。 でも「pendingWrites が true→false になった」みたいな メタデータの変化も取りたいなら👇

onSnapshot(
doc(db, "cities", "SF"),
{ includeMetadataChanges: true },
(doc) => {
// ...
}
);

こうするとメタデータ変化でもイベントが来ます📣 (Firebase)


8) 料金とパフォーマンスの超ざっくり注意 🧾⚠️

リアルタイムは便利だけど、**“読み取りが増えやすい”**のは意識しよう👀 Firestoreは基本的に 返ってきたドキュメント分が読み取りとして計上されます(リスナーも例外じゃない)

なのでコツは👇

  • 🔎 必要な範囲に絞る(where / limit)
  • 📏 一覧は limit() をつける(次の章以降のページングにもつながる)
  • 🧹 画面にいない時はちゃんと解除(無駄な購読をしない)

9) AIで“デバッグ”を早くする(おまけ)🤖🔧✨

AI Change Explanation

9-1) Firebase AI Logicで「変更内容ログ」を日本語で要約させる 📝🤖

「今、何が起きてるの?」をAIに説明させると、初心者の理解が爆速になります🚀

Firebase AI LogicのWeb例(初期化→モデル作成)はこんな形です👇 (Firebase)

import { getAI, getGenerativeModel, GoogleAIBackend } from "firebase/ai";
import { initializeApp } from "firebase/app";

const firebaseApp = initializeApp({ /* ... */ });
const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() });
const model = getGenerativeModel(ai, { model: "gemini-2.5-flash" });

モデルは状況で変わるので、古いモデルの廃止予定があるのも覚えておくと安心です🧯(例:特定モデルのretire案内)(Firebase)

そして onSnapshot の中で docChanges() から「追加/更新/削除」を拾って、 それをAIに食わせて “今起きたこと” を文章で返してもらう…みたいな流れが作れます😊


9-2) Gemini CLI / Antigravityで「原因特定」を早くする 🧠⚡

  • 「購読が増えてる気がする」「useEffectの依存配列これでいい?」みたいな悩みを ログ貼ってAIに突っ込んでもらうのが強いです💪
  • エージェント系ツールは便利な反面、怪しい指示(プロンプト注入)に注意って話もあるので、知らないリポジトリやスクリプトは慎重にね🧯

✅ チェック(できたら次へ!)🎓✨

  • getDocs()onSnapshot() に置き換えた
  • ✅ 別タブで追加したら、一覧が自動で増えた🪄
  • useEffect の cleanup で解除している(超大事)
  • ✅ 「解除しないと購読が増える」を言葉で説明できる

次の第13章は、これを **Reactで安全に運用する型(hooks化・ローディング/エラー/空状態)**に仕上げます⚛️🧯✨