React: 今までの状態管理ライブラリを振り返る

注意
- 自分の観測範囲に基づく個人的な私見や解釈が多分に含まれます。
- ほかの解釈や経験談についてのコメントは強く歓迎します。
ChatGPT o4-mini によるまとめ
これらのライブラリの特徴と課題を踏まえると、状態管理に必要だった要素は大きく以下の5つに集約できると思います。
-
最小限のボイラープレート
-
Redux のように「状態の定義 → アクション → リデューサ → ディスパッチ」という多段階を強いると、単純な CRUD にも過剰なコード量が発生する。
-
→ 宣言的に書けて、利用側が書くコードを極力減らす設計が必須。
-
-
非同期処理との自然な統合
-
Redux はミドルウェア導入しないと非同期が扱いづらく、都度 thunk/saga といった別概念を学習する必要があった。
-
React Query や Recoil Selector のように、フェッチ〜キャッシュ〜ローディング管理を一本化できる API が望ましい。
-
-
派生状態 (Derived State) のシンプル表現
-
MobX の getter、Recoil の selector、Jotai のセレクターは「状態から計算値を自然に定義できる」点で優れる。
-
ただし「どこに・どの粒度で定義するか」を迷わせないガイドラインが必要(粒度が細かすぎると管理コストが増大)。
-
-
状態の粒度とスコープの柔軟性
-
Hooks(useState+Context)はローカル〜グローバルを混在させやすくシンプルだが、大規模化すると副作用ロジックが肥大化する。
-
グローバルストア(Zustand)のように「必要な箇所だけ subscribe できる粒度」と、コンポーネント単位の局所状態が両立できる仕組みが望ましい。
-
-
型安全性・エコシステムとの親和性
-
GraphQL クライアントはスキーマ由来の型生成でランタイムと型の一貫性を担保できる。
-
状態管理も TypeScript や生成済み API 型と自然に組み合わせられることが、信頼性と開発速度向上に直結する。
-

Redux
- Fluxアーキテクチャをベースとしていて、状態の遷移に重きが置かれていた
- 状態管理ライブラリが黎明期だったこともあり、Reactとの組み合わせでよく使われていた
- 特にクラスコンポーネント時代としてコンポーネントで独立した複数の状態を管理したり実装を切り出すことができなかったこともあり、外部化のためにReduxを使っていた
- Single source of truthという考え方で、アプリケーションの状態をすべて単一のストアで管理しようとした
- ただほとんどのアプリケーションではデータを保持するだけで reducer に意味がなく過剰な抽象化だった
- APIリソースごとに状態の単調なCRUD処理を書くことになり、膨大なボイラープレートを生み出していた
- また同期的な状態の更新のみが標準的なサポートで、非同期処理の統合が不完全でわかりづらかった
- コンポーネントへの接続も考えることが多い

MobX
- クラスに加えてデコレーターベースで、関数型指向なReactのコミュニティとしてはあまり受け入れられなかった
- クラスによる状態更新の集約や、getterによる派生状態を率直に表現できていた
- JavaScriptとしては自然なクラスメンバーへの代入がリアクティブになった
- 一部コアなファンもいたが、広く使われることはなかった
- クラスという形で比較的Reactから離れて状態とその更新ロジックをまとまりをもって実装できた
- また多リソースを直接的に表現することにこなれていなかった

useState
- React Hooksの登場とReduxの冗長性から脱Reduxの流れでReactの標準APIへの回帰が起こった
- Reduxよりも自然かつ単純に状態を表現できた
- Contextを組み合わせてグローバル状態を管理したり、単純なローカル状態を扱うにはある程度十分だった
- 今でも比較的使われていることが多いと思う
- シンプルなこともあり、あまり抜き出されることもなくコンポーネント内に直に書かれることが多かった
- そのため生のAPIアクセスの実装を含む
useEffect
を多用することになり一定の冗長さももたらした - 一部で状態の抽象化としてクラスインスタンスをuseStateに入れることもあったが、ミュータブルなインスタンスとイミュータブルを前提とするReactのミスマッチで予期しないバグの温床になっていた

React Query
- サーバーデータのキャッシュを表すのにとても優れた抽象化
-
useQuery
というわかりやすいAPIで、APIアクセスを宣言的に書くことができるようになった - サーバーデータごとに取得から読み込み状態の管理、キーによるキャッシュまでが行われる
- fetcher関数として任意のロジックを書くことができて、そこで単純にAPIアクセスしていた
- そして派生的な状態はコンポーネント内で書くことが多かった

GraphQL, Apollo Client
- クライアント側に構造的なキャッシュを持つことができ、mutationから自動でキャッシュを更新することもできた
- サーバーデータを表すことにこなれていて、標準でスキーマがあることで、型付きのコード生成ができるエコシステムもあり、一時期流行っていた
- 単純に取得ロジックを書かなくともGraphQLクエリを使って
useQuery
でデータが取得できたり、必要なデータを一気に取得することでパフォーマンスだけでなく、データ取得の回数を減らせることで単純に実装が減り単純化もできた - しかしGraphQLの思想に対して十分な理解までは広まらずに表面的な実用性のみが焦点として使われることが多かった
- コミュニティとしてのエコシステムも初期は機能が不十分な部分が多かった
- クエリとしてもグローバルに書かれることが多く、必要になりそうな、ほぼすべてのフィールドをクエリすることが多く、リソースの境界があいまいになるのでREST以上にオーバーフェッチが発生することもあった
- GraphQLの思想としては派生状態をすべてサーバー側で実装して、フロントエンドではその参照をメインとすることでシンプルさを保つものであった
- そのため、バックエンドで実装されるAPIとして伝統的なバックエンドリソースを単に返すのではなく、フロントエンドで必要となる派生データも返すことが望ましかったが
- バックエンドのAPIとして使われるとき、フロントエンドとバックエンドで開発者が分かれていることが多くスキーマ設計の段階からの十分なコミュニケーションが行われることが少なかった
- 実際にはバックエンドのAPIというよりBFFとしてのインターフェースとして設計されたものだと思うがBFFまで作ることは少ないので基本的にバックエンドのAPIインターフェースとして使われた
- また型が生成されるものの、そのままでは子コンポーネントへの受け渡しがあまりこなれていなく、GraphQLの思想として、そのためのFragment Collocationもコミュニティエコシステムでの実装が遅かったこともあり、あまり広まらなかった
- それからボトムアップなFragment CollocationをするためにはコンポーネントごとにFragmentの定義が必要で多くのコードが必要となり、その面でもFragment Collocationを導入できているかで格差が生じていた
- 最近では落ち着いてきている

Recoil, Jotai
- Facebookからのライブラリで、atomベースという新規性のある設計で一躍有名になった
- Reactコンポーネントの外部で動作するuseStateのように使えるので基本的な使い方が理解しやすかった
- 非同期セレクターにより状態管理ライブラリとして初めて実用的なSuspenseとの統合がされた
- 派生状態はかなり自然に定義できたが、設計思想から小さな状態を扱うような使い方をしていた
- グローバルで細粒度の状態管理を目指していたので、atomが増えすぎたり状態グラフがわかりづらくなることもあった
- atomFamilyとして多データを扱うこともできたが、発展的なAPIでそこまで使われなかった
- サーバーデータを扱うにはあまり向いてなかった
- 多くの人が試したが、そこまで広く使われることはなかったと思う
- この時にはすでに React Query が広く市民権を得ていたところもあったと思う

Zustand
- 単純にオブジェクトを保存、更新できるという状態ストアベースでわかりやすい
- グローバルに定義されるストア内にオブジェクトを保存する
- 明示的なset, getによる操作
- Reduxからreducerを取り除いたような設計
- 使っているうちにJotaiが腑に落ちずZustandに移行する事例も見るようになった
- 標準では派生状態を表現することができなく、状態の一部として管理することになる
- ストアごとに永続化するmiddlewareが標準にあり、グローバル状態やフォーム状態の記録には使いやすい
- サーバーデータを扱うには複数のデータを扱うためには Redux と同じように CRUD を実装することになる
- またサーバーデータの取得には明示的な状態更新が必要で、useEffectなどから呼び出す必要がある
- 非同期処理も単純にasync/awaitが使用できる