「 アプリの状態管理については、 最も一般的なアドバイスが、実は最も役に立たないこともあります。それは「一つのグローバルストアを選び、すべてをそこに詰め込む」という手法です。この方法では、API のレスポンス、モーダルの表示状態、保存前のフォーム入力、認証、ナビゲーションフィルタが、すべて同じライフサイクルを持つかのように扱われてしまいます。実際には異なります。Capacitor や Electron アプリケーションは、Webレイヤーとネイティブまたはデスクトップのランタイムにまたがって動作するため、状態は次のように分離する必要があります: データのソース、生存期間、所有権、そしてネットワークやプロセスが消失した際の挙動.
モバイルのランタイムは不安定であるため、状態管理は不可欠な要素となりました。アプリは画面の回転やメモリ不足によって破棄・再生成されることがあり、Android では以下のようなインスタンス状態の復元やライフサイクルコールバックが提供されています: onSaveInstanceState() そのため、以下のドキュメントにある通り、 UC Riversideによるモバイルアプリの状態管理に関する調査。同調査では、 966個のアプリと4,808個のアクティビティを対象に、調査を行った結果、 452個のアプリ(46.8%)に、非空の状態を持つアクティビティが少なくとも1つ含まれていることが判明しました。、一方で 合計1,896個の アクティビティに非空の状態が存在していました。状態管理は、UIが完成した後に付け加えるオプションの抽象化ではありません。それはランタイムにおける契約の一部なのです。
目次
- グローバルストアモデルの再考
- クロスプラットフォームアプリにおけるアーキテクチャパターン
- 永続化とオフラインファーストの同期
- パフォーマンスチューニングとテスト戦略
- Capacitor と Electron のプラットフォーム別ガイド
- モダンなハイブリッドアーキテクチャへの移行
- ベストプラクティスと避けるべきよくあるミス
グローバルストアモデルを再考する

Redux、Context、MobXの比較から始めるのは間違いです。ストアは更新を調整できますが、ある値がサーバー、現在の画面、フォームのワークフロー、あるいはアドレスバーのどれに属するかを判断することはできません。すべての値を一つのグローバルコンテナに詰め込むと、API キャッシュの重複、古いURLパラメータ、そして無関係な画面の再レンダリングを引き起こすサブスクリプションが発生します。
堅牢な設計では、各値に所有者とリカバリポリシーを割り当てます:
- サーバー状態 は、データフェッチおよびキャッシュレイヤーに属します。API のレスポンス、ローディング状態、エラー、鮮度、無効化、リトライはすべてリモートリソースに関するものです。サーバーが常に正となるため、クライアント側に別の永続的な source of truth を持たせるべきではありません。
- クライアントまたはUI状態 は、それを所有するコンポーネントや機能の近くに置きます。モーダルの表示状態、選択中のタブ、展開中の行、テーマ設定、一時的なインタラクションフラグは、通常アプリ全体で永続化する必要はありません。
- フォーム状態 これらは別のワークフローに従います。入力内容、バリデーションメッセージ、変更ステータス、送信の進捗などは、フォーム内での画面遷移後も保持される必要がありますが、それらが自動的に共有のビジネスステートになるべきではありません。
- URLステート これはルーターに属します。検索ワード、フィルタ、ソート設定、選択されたレコード、アドレスバーのページネーションなどは、別のストアにコピーすることなく、ブックマーク、共有、復元、および検証が可能です。
単一のストアがアーキテクチャの足かせになる理由
2024年版 アプリケーション・ステート管理の詳細レビュー Webおよびモバイルアプリケーションにおけるステート管理を検証します。その内容は、散在するUI変数から、明示的でライフサイクルを意識したアーキテクチャへの実用的な移行を反映しています。Androidにおけるinstance-state bundleからViewModelやSavedStateHandleへの進化も同じ方向性ですが、それはすべての値が単一の共有コンテナに属すべきであることを意味しません。
グローバルストアにも明確な役割があります。セッションの識別情報、権限、アプリレベルの設定、接続ポリシー、および慎重に境界を定義した機能横断的なワークフローには、共有の所有権が必要な場合があります。問題は、そのストアが、より適切な所有者が存在する値の「ゴミ捨て場」になったときに発生します。
実践ルール: サーバー、URL、または現在のコンポーネントから再構築できる値については、特定のワークフローで必要とされない限り、グローバルステートには含めないようにしてください。
この境界線は、独立して管理されるモジュールにも役立ちます。製品をデプロイ可能な領域に分割するチームは、以下の原則を適用できます。 マイクロフロントエンド・アーキテクチャアプリケーション全体で一つの依存グラフを構築するのではなく、Capacitor や Electron のコードベースでは、ハイブリッドモデルの方が適しています。具体的には、リモートデータはキャッシュと同期ルールを使用し、UIの値はローカルに保持、そして永続的なワークフローの状態には明示的な永続化を適用します。このように分離することで、書き込みの競合を抑え、リロードやプロセスの停止、オフライン状態からの復帰時における挙動の予測が容易になります。
クロスプラットフォームアプリのアーキテクチャパターン
状態(State)の所有者が決まれば、実装の選択肢はかなり絞られます。クライアント側の状態の多くは、次の3つのパターンのいずれかに当てはまります。 ローカライズされたコンポーネントの状態、 小規模な共有ストア、または イベント駆動型の通信 を、分離されたモジュール間で利用する方法です。どれが万能というわけではありません。更新頻度、所有権、および復旧要件を特定する前にパターンを選んでしまうと、設計ミスにつながるのが一般的です。
| パターン | 複雑さ | メモリ使用量 | 最適なユースケース |
|---|---|---|---|
| コンポーネント内のローカル状態 | 低 | 低 | 画面固有の切り替え、下書き、開閉パネル、一時的な選択 |
| 軽量な中央集権型ストア | 中 | 中 | 共有セッション設定、テーマ、アクティブなワークスペース、機能間UIの連携 |
| イベントバス・アーキテクチャ | 中〜高 | 変数 | 疎結合なモジュール、プラグイン通知、ネイティブブリッジイベント、分離された機能境界 |
ローカルステートをデフォルトにする
コンポーネントローカルな値は依存パスが短くなります。モーダルが開く、行が展開される、フォームの入力が変わるといった際、その機能を所有するコンポーネントだけで更新が完結し、アプリ全体に通知する必要がありません。これにより意図しない結合を防ぎ、ユニットテストも容易になります。また、モバイル端末のスリープ時やデスクトップでの長時間セッションにおけるメモリ保持も抑制できます。
離れた複数の機能で同じ値が必要な場合や、ワークフローがルート境界をまたぐ場合、ローカルステートでは不都合が生じます。その場合は、アプリ全体のシングルトンに置くよりも、機能単位のストアにステートを昇格させる方が適切です。ZustandやPiniaのような軽量なストアを使えば、すべてのコンポーネントが全変更を購読することなく、特定のセレクターや明示的なアクションを公開できます。
トレードオフは規律の維持です。軽量なストアは簡単に作れるため、チーム内でストアが乱立し、所有権が不明確になりがちです。所有者を明確にし、ミューテーションメソッドを定義し、どのコンポーネントからでも書き換えられてしまうようなミュータブルなオブジェクトの露出は避けましょう。
イベントは有用ですが、データベースではありません
イベントバスは、「ネイティブ共有の完了」や「ウィンドウのアクティブ化」、「バックグラウンドタスクによる新データの受信」といった通知には適しています。これにより、プラグインやモジュールが互いをインポートすることなく通信できます。しかし、イベントは一時的なものであるため、ビジネス状態の唯一の記録として使うのは不適切です。サスペンドされたり、アンロードされたり、登録が遅れたりしたサブスクライバーは、メッセージを見逃す可能性があるからです。
イベントは「何かが起きたこと」を知らせるために使い、受信側のモジュールが信頼できるストアやデータレイヤーに問い合わせるようにします。ネイティブとWebの code が個別に進化する可能性があるため、イベント名は限定的なものにし、ペイロードにはバージョンを付けておきましょう。
より広範なアーキテクチャの観点では、 Bridge Globalによるモバイルアプリ開発の知見は、 共有 code とプラットフォーム固有の挙動を比較検討する際に役立ちます。これはアプリの状態管理にも同様に当てはまります。安定しているドメインルールは共有し、ライフサイクルアダプターやネイティブ統合ポイントは分離してください。実用的な モバイルアプリケーションアーキテクチャ では、それらの境界をフォルダ構造や依存関係グラフで明確にする必要があります。
永続化とオフラインファーストの同期
インメモリストアは永続的な状態ではありません。OSによってモバイルプロセスがサスペンドまたは終了されたり、デスクトップユーザーがウィンドウを閉じたり、ミューテーションの実行中にネットワークが切断されたりすることがあります。オフラインファースト設計では、まずユーザーが何を復元できる必要があるかを決定し、その要件に基づいてストレージと同期ルールを選択することから始めます。
永続的な書き込みパスを構築する
信頼性の高いフローでは、即時のユーザー体験とリモートでの確認を切り離します:
- まずはローカルに書き込む。 ユーザーのアクションをローカルのデータベースや永続的なドキュメントストアに適用し、ネットワークを待たずにUIが反応するようにします。
- ミューテーションをキューに入れる。 エンティティID、操作タイプ、ペイロード、作成コンテキスト、リトライステータスと共に操作を保存します。メモリ上だけのキューはプロセスと共に消えます。
- 正確なステータスを表示する。 「ローカル保存済み」「同期待ち」「同期済み」「失敗」を区別します。ユーザーは、変更がデバイス上で永続化されているのか、リモートで確定しているのかを知る必要があります。
- 条件が整った時に同期する。 リスナーの再接続、アプリのフォアグラウンド復帰、スケジュールされたバックグラウンド処理などでリトライをトリガーできます。中断されたリクエストが再送される可能性があるため、同期ワーカーは冪等(idempotent)である必要があります。
- コンフリクトを意図的に解決する。 サーバーのレスポンスで、ローカル操作が受理・拒否・マージされたのか、ユーザーによる確認が必要なのかを判断する必要があります。
ネイティブアプリにおいて、リレーショナルなレコードやトランザクションキューにはSQLiteが実用的です。ブラウザベースのストレージやElectronのレンダラーcodeにはIndexedDBが適していますが、スキーマのアップグレードやトランザクションの境界管理には注意が必要です。シリアライズは明示的に行いましょう。保存すべきはドメインデータとリカバリ用のメタデータであり、コンポーネントのインスタンス、クロージャ、ネイティブオブジェクトへの参照ではありません。

コンフリクトポリシーはプロダクト設計上の決定事項です
Last-write-winsはシンプルですが、正当な編集を破棄してしまう可能性があります。独立したフィールド同士であれば、フィールドレベルのマージが有効です。在庫管理、承認フロー、財務記録、臨床ワークフローなど、自動マージが意味を変えてしまう恐れがある場合は、ドメイン固有のルールを適用するのが安全です。また、同期をブロックしてユーザーに選択を促すべきコンフリクトもあります。
同期レイヤーは次の要素も分離すべきです: 読み取りのリコンシリエーション と ミューテーションのリプレイサーバーデータの再取得だけでは、キューに溜まったミューテーションが成功した証明にはなりません。また、ミューテーションをリプレイしても、結果として得られるレコードが現在のサーバー側の表現と一致する保証もありません。レコードのサーバーバージョンや同等のバリデーターを記録し、構造化されたコンフリクトレスポンスを返し、失敗の原因を説明できるだけの十分なキュー履歴を保持してください。
この設計のユーザー向け部分については、 Vue、Angular、Reactでオフライン画面を作成することは、 インターフェース設計における有用な論点を示してくれます。オフラインモードはコンソールに隠れた例外ではなく、アプリケーションの可視的な状態であるべきです。
以下の動画は、オフライン動作や同期まわりの実装作業を補完するのに役立ちます:
パフォーマンスのチューニングとテスト戦略
状態管理のパフォーマンス問題は、単一の遅いreducerが原因であることは稀です。多くの場合、広範囲なsubscriptionによって無関係なコンポーネントが再レンダリングされたり、派生値が不要に再計算されたり、巨大なオブジェクトグラフが参照され続けたり、同期処理がUIスレッドで実行されたりすることで発生します。どのライブラリが最速かを推測するのではなく、更新の伝播を測定してください。
ダッシュボードを使用した2026年のベンチマークでは、 100個の接続コンポーネントと10,000回のイテレーションを用い、 MobXの測定結果は、 単純な変更で0.3 ms、ネストされた変更で0.4 ms、派生値の変更で0.6 msでした。一方、Redux Toolkitは、 0.8 ms、1.2 ms、1.5 msを 記録しました。また、同じベンチマークの メモリ比較では、Zustandが2.8 MB、Redux Toolkitが4.2 MBでした。 これらはベンチマーク固有の観測結果であり、本番環境での普遍的な保証ではありませんが、subscriptionの粒度や更新戦略がいかに重要であるかを示しています。 React状態管理の比較ベンチマークはこちら テストコンテキスト用。
アップデート境界のチューニング
まずはセレクターから始めましょう。コンポーネントは、セッションオブジェクト全体や API のレスポンス全体ではなく、意味のある最小限のスライスを購読すべきです。計算コストが高い場合は派生データをメモ化すべきですが、すべてのプリミティブを反射的にメモ化してはいけません。メモ化は保持と比較のコストも増えるため、前後でプロファイリングしましょう。
長いリストは仮想化し、更新が個別エンティティを対象とする場合はレコードを正規化し、小さなフィールド変更のために大きなルートオブジェクトを置き換えるのは避けましょう。Electronでは、ウィンドウがモバイル画面よりはるかに長く生き残る可能性があるため、長時間セッションでのレンダラーメモリを監視してください。Capacitor では、入力が集中している間の同期的な永続化を避けてください。ドラフトをデバウンスするか、意味のあるチェックポイントを永続化しつつ、クラッシュしても製品が保持を約束するデータが失われないようにしてください。
Springerに関連する研究では、状態管理のアプローチを変更することで、プログラムの実行時間が テストシナリオ全体で平均17%削減されたことが分かりました。これは、不要な同期や再計算を減らすことで実行時に実質的な性能向上が得られるという考えを裏付けています。この結果は Webアプリケーションの状態管理パフォーマンスに関する研究に掲載されていますが、すべてのスタックで改善が保証されるものではありません。
値だけでなく遷移もテストする
状態テストで確認するのが isLoading リクエスト成功後だけだと、危険なパスを見逃します。次のシーケンスをテストしてください:
- Hydration: 永続化されたデータをロードし、無効なレコードは拒否、不足しているフィールドのみにデフォルト値を適用します。
- Interruption(中断): ミューテーション中にリクエストがキャンセルされたり、アプリがバックグラウンドに移行したりする場合です。
- Replay(リプレイ): キューに溜まった操作を安全にリトライし、サーバー側への副作用の重複を防ぎます。
- Conflict(コンフリクト): サーバーが古いバージョンを拒否した際、UIが復旧可能な解決策を提示します。
- Isolation: ローカルのUI更新が、無関係な機能のレンダリングやミューテーションを引き起こしません。
ネットワーク境界でサーバー状態のモックアダプターを作成し、ワークフロー全体を統合テストで検証します。開発時やCIには、予期しない購読者数、無制限のキュー、機能アンマウント後の状態遷移を検知するためのインストルメンテーションを追加してください。最も価値のあるテストフィクスチャは、単なるReducerの単体テストではなく、現実的なライフサイクルシーケンスです。以下を参照してください: アプリのパフォーマンス最適化ガイド それらの測定値を、再現可能なリリースチェックへと変換する際に。
Capacitor と Electron に関するプラットフォーム別のガイダンス
Webアプリケーションでは、JavaScriptプロセスがモバイルアプリよりも長く実行され続けることを前提とできます。Capacitor と Electron は、それぞれ異なる方法でその前提を覆します。Capacitor はWebレイヤーを iOS や Android が管理するモバイルのライフサイクル内に配置しますが、Electron は、ウィンドウが独立して表示・非表示になるデスクトップのプロセスモデルに Chromium レンダラーを接続した状態を維持します。

Capacitor にはライフサイクルを意識したハイドレーションが必要
バックグラウンドへの移行は、プロセスが再開される保証ではなく、チェックポイントとして捉えてください。アプリの状態が変化した際は、保留中の重要なミューテーションをフラッシュし、現在の同期カーソルを記録し、アクティブなままである必要のないリソースを解放します。フォアグラウンドに戻った際は、失われたデータを再ハイドレートし、認証を確認して、古いサーバーデータをリフレッシュします。また、ローカルストアの準備が整ってからサブスクリプションを再開するようにしてください。
JavaScriptのストアがネイティブプラグインの内部状態を直接管理すべきではありません。カメラセッション、生体認証プロンプト、プッシュ登録、ファイルシステムハンドル、バックグラウンドタスクには、それぞれプラットフォーム固有のライフサイクルルールが存在します。これらをアダプターでラップし、ネイティブのコールバックをドメインイベントやコマンドに変換してください。これにより、アプリがサスペンドされて無効になったネイティブオブジェクトを保持することなく、unavailable、requesting、active、failed、completedといった状態をストアで表現できるようになります。
WebViewの境界があるため、シリアライズも重要になります。Capacitor ブリッジを介してプレーンなデータを渡し、プラグインのレスポンスを検証してください。また、live update によって、インストール済みのネイティブ code とやり取りするWebバンドルが異なる可能性がある場合は、メッセージにバージョンを含めるようにしましょう。 Capacitor がWebとネイティブの code を橋渡しする方法 このアダプター方式が必要となる境界について解説しています。
Electronにはプロセスの所有権が必要です
Electronのメインプロセスは、権限が必要な操作や永続的な調整を担い、レンダラープロセスはビュー固有の状態を管理すべきです。セキュアな設定の読み取り、ファイルの書き込み、ウィンドウの制御などのアクションには、型定義されたIPCコマンドを使用してください。すべてのレンダラーに広範なファイルシステムアクセスを許可したり、特定のウィンドウに送信されたイベントを永続的なアプリケーションレコードとして扱ったりしないでください。
マルチウィンドウアプリケーションには、明示的な同期モデルが必要です。メインプロセスが権威ある更新を配信し、各レンダラーがローカルの表示状態を維持します。2つのウィンドウが同じレコードを編集する場合、単なるブロードキャストイベントではなく、バージョンチェックや競合ポリシーが必要になります。また、閉じられたウィンドウが再開時に状態を復元できるよう、メインプロセスまたは永続化レイヤーが復旧可能なデータのソースである必要があります。
Capacitor と Electron は、ドメインモデル、API クライアント、キュー形式、および reducer を共有できます。ただし、必ずしもライフサイクル code を共有する必要はありません。最も強力なクロスプラットフォームアーキテクチャは、共通の状態語彙を持ちつつ、プラットフォーム固有の永続化、ブリッジ、およびリカバリアダプターを備えたものです。
モダンなハイブリッドアーキテクチャへの移行
レガシーなグローバルストアを書き直す必要はほとんどありません。必要なのは、現状の把握と安全な抽出手順です。移行は、既存のストアを未変更の画面で利用可能なままにしつつ、各機能を一度に一つの状態クラスずつ移動させていく方法が最も効果的です。

技術ではなく、オーナーシップから始める
既存のストアに対してステートカタログを作成しましょう。各フィールドについて、そのソース、コンシューマー、ミューテーションパス、永続化の要否、およびリカバリ動作を記録します。それがサーバー由来か、ルート由来か、フォーム所有か、機能ローカルか、あるいは共有のものかを分類してください。この作業を行うと、グローバルストアの中に、一つの API の背後に隠れた、互いに関連のない複数のシステムが存在していることがよく分かります。
まずはサーバー状態の移行から始めます。手動でミラーリングしている API フィールドを、リクエストステータス、無効化、リトライ、再検証を管理する専用のフェッチ・キャッシュ層に置き換えます。既存の画面を一度にすべて書き換えずに移行できるよう、セレクターは一時的に互換性を維持してください。重複しているサーバー側のコピーは、新しいソースが統合テストに合格した後にのみ削除します。
次に、URLの状態をルーターに戻します。検索フィルターや選択されたリソースは、ルートパラメータやクエリステートを通じて、リロードや共有時にも保持されるべきです。URLの値をストアにコピーし、さらにストアの値をURLに書き戻すような同期エフェクトは削除してください。こうしたループはレースコンディションを引き起こし、ブラウザ履歴の信頼性を損なう原因となります。
機能ごとの状態を段階的に抽出する
モーダルの状態、ウィザードの進行状況、ローカルな選択状態などは、最も近い機能境界(feature boundary)へと移動させます。複数のコンポーネントでその値が必要な場合は、インターフェースを絞った機能専用のストアを使用してください。残ったグローバルストアは、セッションポリシー、テーマ、権限、あるいは明示的に共有が必要なワークフローといった、横断的な関心事のために確保しておきます。
移行期間中は互換レイヤーを活用しましょう。新しいソースから読み込みつつ、従来のセレクター形式を維持することで、フィーチャーフラグを用いた段階的な移行が可能になります。各抽出プロセスを独立してリリースし、エラーパスやハイドレーションの挙動を監視してください。また、新しい所有権モデルの安定性が確認できるまで、ロールバック手段を確保しておきましょう。
Capacitor や Electron チーム向けに、Capgo は署名済みの JavaScript、CSS、設定、アセットのバンドルを特定のチャネル経由で配信できます。ロールアウト制御、バージョン履歴、デバイスログ、採用率や失敗率のメトリクス、自動ロールバック保護も備えています。これにより、ネイティブ側の変更がプラットフォームのリリースプロセスに従っている間でも、状態管理のリファクタリングを段階的に進めることが可能です。運用のメリットは移行をコントロールできることであり、テストや互換性計画を省略する理由にはなりません。
ベストプラクティスと避けるべきよくあるミス
設計レビューの問いが曖昧だと、状態管理のアーキテクチャは破綻します。デザインレビューやプルリクエストでは、以下のチェック項目を活用してください:
- code における所有者を明示する: 「どのモジュールがこの値を変更できるか?また、API はその境界をどのように強制しているか?」を問いましょう。単に2つのコンポーネントが同じフィールドを必要としているという理由だけで追加されたストアは、却下すべきです。
- リカバリの規約を定義する: 永続化される各値について、リロード、アプリの再起動、ログアウト、アカウント切り替え時に、その値が保持されるのかクリアされるのかをドキュメント化してください。例えば、作成中の請求書ドラフトは再起動後も残るべきですが、選択中のワークスペースは再検証が必要になる場合があります。
- 同期の状態を観測可能にする: リクエストID、バージョン、リトライ回数、競合の結果をログに記録しましょう。ユーザーが更新の欠落を報告する前に、リトライの繰り返しや競合による失敗のアラートを設定してください。
- オブジェクトの構造ではなく、コマンドを確認します: Mutationはビジネス上の意図を明示し、入力を検証し、監査しやすいアクション名を公開すべきです。これらのチェックをバイパスする直接的な書き込みは、レビューの議論対象とすべきです。
- 異常なシーケンスをテストします: ナビゲーションによるハイドレーションの競合、書き込み中断後のリトライ、重複配信、2つのウィンドウからのオフライン編集、保留中のリクエスト中のログアウトなどのテストを実行します。
- 削除パスを確認します: 古いセレクター、永続化アダプター、またはイベントリスナーが以前のストアに書き込み続けている場合、マイグレーションは不完全です。両方のソースが同じフィールドを更新できる場合に失敗するテストを追加してください。
実用的なPRでの質問は、「書き込み開始後、承認前にこのプロセスが消失したらどうなるか?」です。その回答には、データの永続性、リトライの所有権、重複排除、およびユーザーに表示される失敗状態が含まれている必要があります。
Capgo は、CapacitorJS および Electron チームが、署名済みbundle、ロールアウト制御、デバイスレベルのlogs、およびロールバック保護を備えたターゲットチャネルを通じて、JavaScript、CSS、設定、アセットの更新を配信するのに役立ちます。使用して Capgo 状態管理のリファクタリングやリカバリ修正を段階的にデリバリーし、ライフサイクルと同期テストで検証しましょう。