【ITニュース解説】Firestore Data Modeling for Offline-First React Native Apps
2026年09月08日に「Dev.to」が公開したITニュース「Firestore Data Modeling for Offline-First React Native Apps」について初心者にもわかりやすく解説しています。
ITニュース概要
Firestoreでオフラインに強いReact Nativeアプリを作るには、適切なデータ設計が重要だ。誰が書くか、書き込み頻度でドキュメントを分割し、共有データは分離する。最初の画面は少ない読み取りで表示。競合データはサーバーで管理し、クライアントはイベントを送る。実機でのオフラインテストも必須だ。
ITニュース解説
Firestoreは、Googleが提供する高速でスケーラブルなデータベースサービスで、リアルタイムなデータ同期やオフラインでのデータ利用が特徴だ。多くの開発者はFirestoreの「オフライン永続化」機能が有効になっているため、アプリが自動的に「オフラインファースト」に対応していると考えがちだが、これは誤解だ。オフラインファーストのアプリとは、ネットワーク接続がない状態でもユーザーが快適にアプリを使えることを指し、その実現にはFirestoreのデータの保存方法(データモデリング)が極めて重要になる。データの設計を適切に行えば、Firestoreがオフライン処理の大部分を自動で行ってくれるが、設計を間違えると、アプリに読み込み中の表示や再試行のロジックを追加する手間が大幅に増えてしまうだろう。
Firestoreのオフライン機能の核となるのは、データのキャッシュと書き込みのキューイングだ。Firestoreは一度読み込んだドキュメントや実行したクエリの結果をデバイスのローカルにキャッシュする。しかし、ユーザーが一度もアクセスしていないドキュメントはキャッシュされていないため、オフライン時には利用できない。このため、アプリの重要なデータは、ユーザーがアクセスする前に、できるだけ少ない読み込みで事前に取得するようなデータ構造を考える必要がある。
また、オフライン中のデータ書き込みは一時的にキューに格納され、デバイスがオンラインに復帰した際に順次Firestoreサーバーに送信される。この時、同じフィールドに対して複数のデバイスから書き込みがあった場合、最後に書き込まれたデータが優先される「最終書き込み優先」というルールで競合が解決される。しかし、異なるフィールドへの書き込みであれば、問題なくマージされる。この「誰が、何を、どのように書き込むか」という挙動を理解することが、ドキュメントの分割方法を決定する上で非常に重要となる。
データモデリングの最初の原則は、データを「何であるか」ではなく「誰が書き込み、どのくらいの頻度で書き込むか」で分割することだ。例えば、ユーザーのすべての情報を一つの大きなドキュメント(users/{uid})にまとめるのではなく、ユーザーのプロフィール(ユーザーがめったに更新しない情報)、学習進捗(ユーザーが常に更新する情報)、アイテムごとの復習状態(オフラインでの書き込みが多い情報)、そして統計データ(サーバー側のみが更新する情報)のように、それぞれ独立したドキュメントやサブコレクションに分割する。
この分割はオフライン時のアプリの安定性に大きく貢献する。例えば、学習進捗のような頻繁に更新される小さなドキュメントは、オフライン書き込みが競合しにくく、クリーンにマージされやすい。また、統計データのようにサーバー側のみが書き込むドキュメントは、クライアント側からの書き込みキューが発生しないため、競合の心配がない。プロフィールのような安定したデータは、頻繁に更新されるデータで「汚される」ことがなく、効率的にキャッシュされ続ける。もし、これらすべてを一つの大きなドキュメントにまとめてしまうと、学習進捗の更新とサーバーからの統計更新が同時に行われた際に競合が発生したり、わずかな変更でドキュメント全体が再送信・再キャッシュされる無駄が生じたりする。
アプリの起動時、ユーザーが最初に目にするホーム画面の表示に必要なデータは、できるだけ少ない読み込みで取得できるように設計するべきだ。理想的には、一つのドキュメントの読み込みでホーム画面の情報をまかなうことが目標となる。例えば、今日の課題数、連続記録、次のレッスンのポインターといった情報は、すべて一つのドキュメントに集約されるようにする。これは意図的にデータを「非正規化」、つまり重複した形式で保存することを意味する。ユーザーがレッスンを完了した際に、サーバー側の関数(Cloud Functions)が完了情報と共に次の課題数などをそのドキュメントに更新するようにすれば、クライアントは複数のクエリを実行して情報を集める手間が省ける。さらに、データがキャッシュから読み込まれたかどうかや、未同期の書き込みがあるかどうかを示す情報(hasPendingWritesやfromCache)をアプリのUIに表示することで、「このデバイスに保存されています。オンラインになったら同期されます」といったヒントをユーザーに提供でき、読み込み中のまま止まっているような印象を与えるのを防げる。
ユーザーがアプリ内でアイテムごとに状態を蓄積するような機能、例えばスペース反復学習の進捗、ウォッチリスト、ブックマーク、完了したレッスンなどは、ユーザーのドキュメント内に配列として保存したくなるかもしれないが、これは避けるべきだ。配列はデータ量が増えやすく、変更のたびに配列全体が書き換えられるため、複数のオフラインデバイスが同時に異なるアイテムを追加しようとすると、互いの変更を上書きし合う競合が発生する可能性が高まる。代わりに、サブコレクションを使用し、アイテムごとに個別の小さなドキュメントとして状態を保存する。これにより、例えば「今日期限の復習アイテム」といったクエリは効率的になり、Firestoreのキャッシュも活用しやすくなる。毎日の同じクエリに対して、Firestoreはキャッシュを温かく保ち、オフライン時でもユーザーは今日やるべき復習を見ることができ、オフラインで行われた更新は、小さなドキュメント単位の変更としてオンライン復帰時に安全に同期される。
アプリ内の語彙リスト、レッスン内容、映画カタログなど、すべてのユーザーで共有される固定的なコンテンツは、ユーザー固有のデータとは明確に分離すべきだ。これをユーザーのドキュメントに含めると、すべてのユーザーのキャッシュが同じコンテンツのコピーで膨らみ、コンテンツを更新するたびに多数のユーザーのドキュメントを書き換えるという非効率な処理が発生する。共有コンテンツは独自のコレクションに保存し、バージョン管理を行うのが良い方法だ。アプリ起動時にコンテンツのメタデータ(現在のバージョンなど)を読み込み、デバイスにキャッシュされているバージョンと比較し、必要であれば新しいコンテンツのみを取得するようにすれば、効率的なキャッシュ利用が可能になる。コンテンツはバージョンごとに不変にすることで、オフライン時にユーザーが古いバージョンのコンテンツを見ていても、それが壊れることなく利用できる。
リーダーボードの順位、フォロワー数、ランキングなど、複数のユーザーが同時に影響を与える可能性のある「競合しやすい」データは、クライアント側で直接更新するのではなく、必ずサーバー側で計算・集計し、クライアントはそれを読み取るだけにすべきだ。クライアントが直接共有カウンターを増やすような処理を行うと、オフライン状態のデバイスが数時間遅れて書き込みを再生した際に、計算結果が食い違ったり、数字が正確に合わなくなったりする問題が発生する。適切な方法としては、クライアントは「イベント」(例えば「チャレンジを完了した」)を書き込み、Cloud Functionsのようなサーバー側のサービスがそのイベントを処理し、集計結果を統計情報やリーダーボードに反映させる。この際、イベントのタイムスタンプはデバイスの時刻ではなく、サーバー側で付与されるserverTimestamp()を使用することで、タイムスタンプの信頼性とイベントの順序を保証できる。
ソーシャルフィードやコンテンツフィードの場合、オフラインのユーザーは、最後に読み込んだページの内容を見られるようにするべきだ。これは、フィードのクエリを安定させ、カーソルベースでページネーションを行うことで実現できる。フィードの各投稿ドキュメントには、表示に必要な情報(投稿者名、アバター、いいね数など)を「非正規化」して含めておくことで、キャッシュされたフィードを表示する際に、オフラインで失敗する可能性のある追加の読み込みを発生させずに済む。次のページを読み込む操作は、ユーザーがオンラインの場合のみ可能にするのが良いだろう。
Firestoreのセキュリティルールは、書き込みがオフラインでキューに入れられた時ではなく、サーバーで再生される時に実行される。したがって、デバイスがオフライン中に書き込みを受け付けたとしても、オンラインになった際にサーバー側のルールによって拒否される可能性がある。このような事態を避けるためには、ルールをシンプルに、そして「所有者ベース」(例えば、ユーザーは自分のデータのみ書き込み可能)に設定し、クライアントが拒否されるような書き込みを容易にキューに入れないようにすることが重要だ。また、書き込みが拒否された場合に備え、失敗した書き込みをログに記録するリスナーを設定しておくと、問題の追跡に役立つ。
最後に、オフライン機能のテストは非常に重要であり、シミュレーターのネットワーク切り替えだけでは不十分だ。実際のデバイスでテストを行う必要がある。具体的には、まずオンライン状態でアプリを開き、一度アプリを終了させる。次に機内モードを有効にしてアプリを再起動し、オフライン状態で5分間程度アプリを使用する。その後、再びオンラインに戻し、データがどのように同期されるかを注意深く確認する。Firestoreコンソールでデータの整合性を確認し、意図しないドキュメントの変更がないかチェックすることも忘れてはならない。さらに、同じアカウントで2台のデバイスを使用し、オフラインでの競合がどのように発生するかをテストすることも有効だ。これらのテストで見つかるバグは、ほとんどが「ユーザーがまだ読んでいないドキュメントが必要な画面」「クライアントが直接インクリメントしていた共有カウンター」「上書きされてしまった配列」といったデータモデリングの誤りに起因するものであり、Firestore自体の問題ではない。
以上のように、オフラインファーストのReact NativeアプリでFirestoreを使う場合、ドキュメントを「誰が書き込み、どのくらい頻繁に更新されるか」で分割し、起動時の読み込みを最小限に抑え、項目ごとの状態はサブコレクションで管理する。共有コンテンツはユーザーデータから分離し、競合するデータはサーバーサイドで処理する。そして、厳格なオフラインテストを繰り返し行うことが成功の鍵となる。