React の状態管理の歴史と、最新 API に込められた React の思想
はじめに
React が誕生して10年以上が経ちましたが、「状態管理」の議論は今も終わりません。Redux、Recoil、Zustand、そして useSyncExternalStore。なぜこれほど多様なライブラリが生まれ、React 公式はついに"標準 API"を出したのでしょうか?
本記事では、「状態管理の歴史」と「React が見ている世界」を整理しながら、最新 API が登場した背景まで深く掘り下げます。また、この内容は JSConf.jp 2025 Pre Event で話した内容の補足や詳細です。
この記事の読み方
記事全体を通して読むことで状態管理の歴史が体系的に理解できますが、すでに知っている部分は読み飛ばしても問題ありません。
| あなたの状況 | おすすめの読み始め |
|---|---|
| React の状態管理について基礎から学びたい | 1. React における「状態」とは何か? から |
| Redux は使ったことがある | 5. Recoil の登場 から |
| Recoil や Zustand も知っている | 7. useSyncExternalStore の登場 から |
| 結論だけ知りたい | 9. まとめ へ |
tl;dr
- 状態管理は UI = f(state) という React の思想を支える根幹
- Redux → Recoil → Zustand と時代ごとに異なる課題を解決してきた
- useSyncExternalStore は公式が提示した"外部ストアとの正しい同期"
- React の理想は「状態はアプリの外にあり、React は同期する」
なんでこんなことが気になったのか?
2019年ごろからReactに触れ始めました。
それまではScalaでバックエンドを書いていたこともあり、Statelessであることが当たり前で、良い設計だと思っていました。
ところが、ReactとFluxの考え方に出会ったとき、「状態」というものに改めて向き合うことになりました。その奥深さに惹かれました。
Webアプリケーションを使う中で自然と生まれる「状態」。それと真正面から向き合うと、疑問が湧いてきます。
- Reactらしい状態のあり方とは何だろう?
- 自分より頭のいいエンジニアたちは、状態をどう捉えているのか?
- 状態管理ライブラリは、どんな思想でこの問題に立ち向かったのか?
そんな折、JSConfJP2025のプレイベントで登壇の機会をいただきました。
自分の興味が些細なものだったとしても、そこを起点に、Reactやフロントエンドの状態管理について少し先の未来を予想してみたい。そしてワクワクしたい。そう思っています。
JSConfJP2025 プレイベントでの発表
本記事は、以下の登壇内容を補足しつつ、スライドでは語りきれなかった背景を解説します。
スライド:
1. React における「状態」とは何か?
ライブラリや API の各論に入る前に、そもそも React における状態について理解していきましょう。
1.1 UI は「状態の投影」である
React でいう「状態(state)」とは、一言でいうと 「時間とともに変化しうる値のうち、UI に影響を与えるもの」 です。
逆にいうと、以下のようなものは React の意味での状態ではありません。
- 一度決まったら変わらない定数(例:アプリ名、固定の文言)
- UI に影響を与えない値(例:ログ用にしか使わないカウンタ)
React は「UI を直接いじる」のではなく、状態を変えることで UI が結果として変わるという世界観を採用しています。これを数式で表すと、よく出てくる UI = f(state) という形になります。
ここで大事なのは、UI が「原因」ではなく 「結果」 だということです。
1.2 状態の種類
React の状態をざっくり分けると、以下の3種類になります。
- ローカル状態 — コンポーネントの中だけで完結する状態(入力フォームの値、モーダルの開閉フラグなど)
- 共有状態 — 複数コンポーネントで共有される状態(ログイン中のユーザー情報、カートの中身など)
- 派生状態(derived state) — 他の状態から計算で求められる値(合計金額、フィルター済みリストなど)
React は、「どの状態が UI に影響するか」「どこまでを React に任せるか」を開発者が意識して設計することを前提としています。その設計の仕方がまさに「状態管理」のテーマそのものです。
1.3 HTML を直接いじらないという発想
従来の jQuery 的なスタイルでは、ボタンがクリックされたら DOM を取得し、クラスを追加・削除して見た目を変える——というように、UI を直接操作 することが一般的でした。
React の発想はこれとは逆です。
- 「今の状態」をもとに UI をまるごと"描き直す"としたらどうなるか?
- その結果だけを DOM に反映する
このとき、開発者は DOM をいじるのではなく、「状態」と「それに応じた UI の定義」に集中できます。
1.4 状態は「真実の源泉(source of truth)」
React における状態は、UI を描画するための唯一の参照元です。状態が変われば UI は再評価され、状態が変わらなければ UI も変わらない、というシンプルな関係性が保たれます。
この「状態が真実の源泉であり、UI はその投影にすぎない」という考え方は、後に登場する Redux や Recoil、Zustand、そして useSyncExternalStore にも共通して流れている根本的な思想です。
2. React における状態管理が難しくなる理由
React が定義する「状態」は UI に影響を与える変化しうる値でした。では、なぜこの「状態」を管理することが難しいのでしょうか?
理由は、状態が必ず時間・コンポーネント構造・アプリケーションの規模という3つの軸と向き合うためです。これらは以下の3要素に集約されます。
2.1 同期:どのタイミングで変わるのか
状態はユーザー入力、API の完了、WebSocket の更新など、予測しづらい"外界の出来事"によって変化します。
そのため UI を正しく描画するには、
- いつ状態が変わるのか
- 変わったことをどのコンポーネントが知る必要があるのか
- その通知は遅延しても良いのか、同期的であるべきなのか
といった時間的な一貫性を保つ必要があります。React が"宣言的 UI"であっても、この同期の問題はアプリケーションの根本に残り続けます。
2.2 共有:誰が読み、誰が書くのか
コンポーネントはツリー状にネストして増えていきます。その中で状態にはスコープの問題が生まれます。
- ローカルコンポーネントだけで使う状態
- 複数コンポーネントで共有したい状態
- 子コンポーネントで更新したい親の状態
UI が複雑になるほど、「誰が状態を持つべきか?」を誤ると props drilling や不要な再レンダリングが多発し、アプリ全体が壊れやすくなります。
2.3 永続:どこまで保持する必要があるのか
状態は「保持する期間」も重要です。
- タブをまたいで保持したい?
- ページ遷移後も残したい?
- 一時的な入力値はどう扱う?
- ブラウザに保存してもよい?サーバー側と同期すべき?
こうした要件はアプリの規模に比例して増えていき、単純な useState では収まりきらず外部ストアが必要になる場面が増えていきます。
3. React 初期の時代と課題
ここまで紹介した複雑性は、React が発表された当初(2013〜2016年)において、UI ライブラリとしての React が意図的にカバーしていなかった領域でもありました。
React は UI を宣言的に表現する力は圧倒的だったものの、状態管理という領域は"必要最小限しか手を出さない"立場をとっていました。その結果、以下のような課題が噴出していきます。
3.1 props drilling の深刻化
状態を上位コンポーネントに置かざるを得ないが、子孫コンポーネントに渡すには props を何段階もバケツリレーする必要がありました。
- ある小さな UI 変更のために大量の props 受け渡しが発生
- コンポーネントの再利用性低下
- ツリーが深くなるほど破綻
3.2 グローバル状態の扱いが未整備
当時の React には Context API がまだ曖昧で非推奨扱いに近く、アプリ全体で共有すべき状態をどこに置くか? という問いに対する"公式な答え"が存在しませんでした。
3.3 外部ストア(Flux/Redux)がほぼ必須に
その結果、
- アプリの状態は React の外に置く
- React は UI を描画するだけに専念する
- 状態は Flux/Redux が管理する
というアーキテクチャが標準解として広まりました。
つまり React 初期の状態管理は、React 本体には足りないものがあり、外部ライブラリがそれを補う時代だったのです。この歴史的背景が、のちに Redux → Recoil → Zustand へと続いていきます。
4. Redux の時代:明確なデータフローの勝利
React は UI を宣言的に書くためのライブラリとして登場しましたが、アプリケーションが大規模化しはじめた2014〜2016年頃、「UI はわかりやすいのに、アプリ全体の状態が複雑化していく」 という問題が急速に顕在化していきます。
当時はまだ Context API も未整備で、Hooks など影も形もなく、状態の"共有"と"変更の一貫性"を担保できる仕組みは存在しませんでした。この混乱を整理しようとして登場したのが、Facebook が公開した Flux アーキテクチャ、そしてその考え方を洗練した Redux でした。
4.1 時代背景:MVC の限界と複雑化する Web アプリ
当時のフロントエンドはまだ jQuery 全盛期の名残があり、多くの開発者は「MVC」の問題に悩まされていました。
- どこでも状態をいじれてしまう
- 双方向バインディングにより状態の流れが不透明
- UI がどの状態に依存しているか追えない
- デバッグが困難
これらの課題は、Facebook のような超大規模アプリケーションでは致命的でした。React が登場し UI は整理されたものの、状態の流れ(データフロー)を整理する方法が依然として不足していました。
4.2 Flux:React らしい"一方向データフロー"という発明
Facebook が2014年に提案した Flux は、UI を安定させるには、データは常に上流 → 下流の一方向に流れるべきというシンプルな原則に基づいていました。
Flux は以下の要素を持ちます。
- Action — 変更したい内容を表すイベント
- Dispatcher — イベントを適切なストアに配送
- Store — 状態(データ)の保持場所
- View (React) — 状態を受け取り UI を描画する
これにより、状態の変更は常に"どこからどう流れて UI に届くか"が明確になりました。
しかし Flux は"設計モデル"であり"実装ライブラリ"ではなかったため、各プロダクトが独自の Flux 風実装を持つ混乱も生まれました。
4.3 Redux:Flux を極限までシンプルにした世界標準
2015年、Dan Abramov と Andrew Clark によって生まれた Redux は、**Flux の複雑さを徹底的に削ぎ落とした「最小の状態管理」**でした。
Redux の原則はたった3つです。
- アプリケーションの状態は単一の store に集約される
- 状態は読み取り専用であり、必ず Action 経由で更新される
- 状態の更新は pure function(Reducer)で行う
この「制約による自由」こそ、Redux が当時のフロントエンド界を席巻した理由です。
- どこでも勝手に状態をいじれない
- 変更は必ず Action → Reducer の流れ
- 状態の変更が完全に追跡可能
- 時系列を保存できるため"タイムトラベル"が可能
Flux の煩雑さを捨て、"状態の正しさ"を保証する設計だけを残したのが Redux でした。
4.4 Redux がもたらした SPA 革命
Redux の登場は、SPA(Single Page Application)の開発に革命を起こしました。
状態が一元管理され、アプリの構造が読みやすくなった
Redux の store は「アプリの真実の源泉(single source of truth)」です。「この値はどこが持ってる?」「どのタイミングで変わる?」「どうしてこの UI になっている?」といった疑問が一気に解決され、大規模開発におけるメンタルモデルが劇的に安定しました。
DevTools による"タイムトラベル"が衝撃的だった
Redux DevTools はフロントエンド開発に革命を起こしました。Action の発火履歴がすべて記録され、任意の時点に"巻き戻し"でき、状態変更の理由が100%トレース可能になりました。これは従来の MVC ではあり得ない発想で、React + Redux の組み合わせは"デバッグ可能な UI 開発"を実現しました。
非同期処理の扱いをモデル化(thunk / saga など)
Redux 自体は同期処理しかサポートしませんが、Redux-thunk や Redux-saga といったミドルウェアが登場し、API の取得、非同期アクション、エラーハンドリングなどをアーキテクチャレベルで統制できるようになりました。
4.4.1 Redux をはじめて本番投入したときの気持ち
当時を振り返ると、正直なところ「Reduxは革命だ!」と感動して使い始めたわけではありませんでした。どちらかといえば「長い物には巻かれろ」という感覚で採用した記憶があります。本当にReduxが必要だったのか、今思えば怪しいです。でも、当時の自分にはそれ以上の解決策が見つからなかったのも事実です。この頃の自分には状態管理を掘り下げてライブラリを作ってみる技術体力がありませんでした。
ただ、振り返るとメリットもありました。Reduxは厳密であるがゆえに、Fluxのデータフローを強制的に叩き込まれることになりました。Action、Reducer、Store——この一方向の流れを身体で覚えたことが、その後の状態管理を考える上での思考の軸になったと感じています。
4.5 Redux が抱えた課題(次世代への布石)
Redux は強力でしたが、完璧ではありません。
- ボイラープレートが多い(Action 型定義、Reducer、Store 構築…)
- 状態更新はすべて純粋関数に閉じないといけない
- コードが儀式的で"型"の導入も重くなりやすい
- 複雑なアプリになるほど Reducer の管理が難しい
また、React が Hooks や Context API を整備していくにつれ、「React らしい書き方と Redux の思想がズレている」 という認識が広がっていきます。
この課題が後に Recoil や Zustand が登場する土壌となり、さらには React 公式が useSyncExternalStore を提示する理由へとつながっていきます。
5. Recoil の登場:宣言的 React アーキテクチャの始まり
Redux の成功により、フロントエンドは「状態管理は外部ストアが担う」という常識が広まりました。しかし、アプリがさらに複雑化し、React 本体の設計が進化するにつれ、Redux が抱えていた"構造的な制約"も次第に目立つようになります。
- すべての更新を Action → Reducer という手続きに従わせる必要がある
- 純粋関数に閉じた更新モデルが大規模アプリには重くなる
- 依存関係を宣言的に表現する手段がない(すべて手動で管理)
- コンポーネント単位の状態とアプリ全体の状態が分断されてしまう
これらは React 自身の進化とも密接に関係していました。
5.1 開発背景:React の未来を見据えた"次のモデル"が必要だった
2018〜2020年頃、Facebook(現 Meta)の内部では Messenger や Instagram といった超大規模 SPA が React で運用されていました。
この規模になると Redux の"単一ストア + 手続き的更新"モデルでは次第に以下の限界が浮き彫りになります。
- 状態が巨大なツリーになるため一部だけ効率的に更新したい
- コンポーネント間での依存関係が複雑化し、手動で管理できない
- 派生状態(計算結果)を最適化したいが、Redux には標準の仕組みがない
- 非同期・キャッシュ・並列レンダリングといった新しい課題に対応できない
さらに追い打ちをかけたのは、Facebook 内部で進んでいた Concurrent Rendering / Suspense といった React の大規模刷新でした。これらの新機能は、状態を「宣言的な依存関係のネットワーク」として扱える設計が必要であり、既存の Redux / Flux モデルとは相性が悪かったのです。
この背景のもと Meta のエンジニアたちが「次世代 React に自然に寄り添う状態管理」を模索し、実験的ライブラリとして生まれたのが Recoil(2019) でした。
5.2 Recoil が提示した「React らしい状態管理」
Recoil が革新的だったのは、状態を**"ただのデータ"ではなく"宣言的な依存グラフ"**としてモデル化した点です。
Recoil の中心概念は3つだけです。
- アトム(atom) — 最小単位の状態(useState の"外"に置いた値)
- セレクタ(selector) — 依存関係を宣言し、自動で最適化される派生状態
- 依存グラフ — アトムとセレクタの関係を Recoil が内部で解析・再計算
このモデルが画期的だった理由を見ていきましょう。
状態間の依存関係を"明示的に"書ける
Redux では状態が変化した際の伝播はすべて手動でした。Recoil では依存関係を宣言すると、変更に応じて Recoil が自動で再評価し、必要なコンポーネントだけを最小限に更新します。
派生状態(derived state)が一級市民
計算結果としての状態を関数で表現できるのは React と極めて相性が良いポイントです。
const filteredList = selector({
key: "filteredList",
get: ({ get }) => {
const list = get(todoListState);
const filter = get(filterState);
return list.filter(item => item.status === filter);
}
});
UI = f(state) の"f"を自然に書けるようになったと言ってもいいでしょう。
React のレンダリングモデルと調和
Suspense との統合、非同期の計算、データフェッチとの接続、コンポーネントの境界を跨いだキャッシュ——こういった"React の未来像"にフィットするよう設計されたのが Recoil でした。
5.3 Recoil が象徴したもの:命令から宣言へ
Redux は状態を"手続き"で管理する思想でした。Action を発行して、Reducer が解釈して、新しい状態を返却する。
一方で Recoil は、状態と派生関係を"宣言"すれば、あとは Recoil が最適な更新を行う。React と同じ宣言的思想に基づいています。
これは React の UI モデルと極めて近い発想であり、Recoil は「宣言的な状態管理」という新しい潮流の先駆けとなりました。
6. Zustand の時代:DX と柔軟性の台頭
Recoil は「宣言的で React に寄り添う状態管理」という新しい潮流を生み出しました。しかし、すべてのアプリケーションが Recoil の"依存グラフモデル"を必要としていたわけではありません。
React 本体は Hooks や Context が整備され、開発者は「状態と UI の距離感」をより自由に選べるようになりました。
その結果として生まれたのが、もっとシンプルに書きたい。もっと素の JavaScript で書きたい。状態管理は重すぎず、React と自然に繋がるだけでいい。 という新しいニーズです。
このニーズに応える形で登場したのが Zustand(ドイツ語で"状態"の意味) です。
6.1 開発背景
Redux は厳格で強力だが"儀式的"。Recoil は未来的でパワフルだが"学習コストが高い"。
一方で、実際の現場の多くのユースケースはもっとシンプルでした。
- 小規模〜中規模アプリで、アプリ全体のデータフローを厳密に管理したいわけではない
- 派生状態や依存グラフよりも、まずは少量のグローバル状態を簡単に扱いたい
- Recoil のような専用 DSL(atom/selector 構文)を覚えたくない
- Redux のように Action/Reducer を定義したくない
つまり、"React の useState をそのままグローバルに拡張したい" という埋もれたニーズが一気に顕在化したのです。
6.2 Zustand が評価された理由:Just JavaScript の思想
Zustand の強みは、驚くほどシンプルな API にあります。store 定義は、ほぼ「ただの関数」です。
最小限の例
import { create } from "zustand";
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
React コンポーネントでは、まるで useState を使うかのように読めます。
function Counter() {
const { count, increment } = useStore();
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
この"自然さ"が Zustand 最大の魅力です。
6.3 Zustand の特徴が React と相性抜群だった理由
メンタルモデルが薄い(学習コストがほぼゼロ)
Zustand は「ただの JS オブジェクトを返す関数」なので、React Hooks の延長で書けます。Redux の"Reducer"や Recoil の"Atom/Selector"のような新しい概念を覚える必要がありません。
Hook ベースで自然に状態を読む
useStore() が返すのは単なる値。React の Hooks 文化と完全にマッチした設計です。
ミドルウェアが composable(拡張しやすい)
Zustand はミドルウェアによって必要な機能を後付けできます。ローカルストレージ保存、Immer によるイミュータブル更新、Redux DevTools との連携、subscribe による外部反応など。
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
const useBearStore = create(
devtools(
immer((set) => ({
bears: 0,
addBear: () => set((state) => {
state.bears++;
})
}))
)
);
必要なものだけを composable に追加できる柔軟性が特徴です。
パフォーマンスが高い
Zustand は内部で subscribe ベースの外部ストアとして動作しており、レンダリング最適化に強い構造になっています。実は、React 公式の useSyncExternalStore が登場する以前からその思想を先取りしていたとも言えます。
6.4 「素の JavaScript の延長としての状態管理」が受け入れられた理由
コミュニティが求めていたのは、ルールに縛られる状態管理ではなく、React と自然に繋がり、学習コストが低く、必要に応じて機能を足せる柔軟性でした。
Zustand はまさにそれを実現し、「軽量・柔軟・React と相性が良い」状態管理のデファクトスタンダードとして広く受け入れられていきます。
6.4.1 Zustand に出会い直したときの気持ち
正直に言うと、Zustandを導入しようとしたとき、メンテナが日本人(Daishi Kato氏)であることを知りませんでした。「Recoilの開発が続いていれば、それでいいだろう」くらいに思っていたほどです。
しばらくの間、状態管理の面白さや奥深さを忘れていました。「動けばいい、開発が回ればそれでいい」そんな感覚だったと思います。しかし、日本人開発者が作っていること、そしてその開発思想に触れたことで、興味が再燃したのを覚えています。
また、時間が経って自分自身も技術的に成長し、以前とは違う視点でこのライブラリを見られるようになりました。それも個人的には嬉しいことでした。
6.5 Recoil と Zustand の違いが示すもの
- Recoil — 状態の依存関係を宣言的に最適化する理想的モデル
- Zustand — 必要最低限のグローバル状態を素直に扱えるミニマル設計
どちらも React が見ている未来と矛盾しないが、アプローチが異なるからこそ両者は補完的に存在し続けています。
そしてこの流れは、後に React 公式が提示する useSyncExternalStore という"共通の低レイヤ API"へとつながり、状態管理の世界をさらに大きく動かしていくことになります。
7. React 公式の回答:useSyncExternalStore の登場
Zustand や Redux などの"外部ストア型"の状態管理は、React の進化とともに広く利用されるようになりました。しかし React 18 で導入された Concurrent Rendering(並列レンダリング) の世界では、外部ストアと React のあいだに深刻な不整合が生じることが分かってきます。
これを受けて React 公式が提示したのが useSyncExternalStore(2022 / React 18) でした。
これは単なる新しいフックではなく、「外部ストアとの正しい同期を保証するための低レイヤ API」 であり、10年にわたる状態管理の歴史を踏まえた"React 公式の回答"とも言える存在です。
7.1 開発背景:Concurrent Rendering がもたらした新しい課題
React 18 では描画処理が中断可能・再開可能・非同期評価可能になりました。これは UI の滑らかさや応答性を向上させるために不可欠ですが、外部ストアと組み合わせると次のような問題が起こります。
問題:外部ストアの変更を React が見逃す
たとえば以下のようなフローを想像してください。
- React があるコンポーネントのレンダリングを開始
- 外部ストアが更新される
- React がその更新通知を受け取る前にレンダリングを再開
- 古い(不正確な)状態で UI が表示されてしまう
つまり、外部ストアの更新が Concurrent Rendering とズレるのです。Redux や Zustand は内部で subscribe ベースに構築されているため、この問題が顕著でした。
7.2 useSyncExternalStore の役割:React と外部ストアの正しい橋渡し
そこで導入されたのが useSyncExternalStore です。React 公式が保証する役割は次の通りです。
- 外部ストアを React が安全に同期(subscribe/unsubscribe)できる
- 現在の状態をスナップショットとして読む
- Concurrent Mode(中断可能なレンダリング)でも正しいタイミングで再評価される
つまり、"React が外部ストアを読む時の唯一の正しい方法" を標準化したのが useSyncExternalStore なのです。
7.3 コードで見る:useSyncExternalStore の基本構造
たとえば「カウントを持つ外部ストア」を自前で用意する場合、従来は subscribe 機能を React に直接渡していましたが、これは非推奨となりました。代わりに以下のように記述します。
外部ストアの実装例
// countStore.ts
let count = 0;
const listeners = new Set<() => void>();
export function getSnapshot() {
return count;
}
export function subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function increment() {
count += 1;
listeners.forEach((l) => l());
}
React コンポーネント側
import { useSyncExternalStore } from 'react';
import { subscribe, getSnapshot, increment } from './countStore';
export function Counter() {
const count = useSyncExternalStore(subscribe, getSnapshot);
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
これにより、React 18 のどんなレンダリング戦略が働いても、最新の状態を読み、更新通知を正しいタイミングで受け取り、レンダリングが古い状態で破綻しないことが保証されます。
Redux や Zustand などの主要ライブラリも内部的にはこの API を採用するようになりました。
8. React が見ている未来:状態は「外」にある
useSyncExternalStore の登場は、React がこれから向かう方向性をはっきりと示しています。それは、React 誕生以来一貫してきた哲学——React は UI のためのライブラリであり、状態の所有者ではない——という原則を、改めて強固にする動きです。
8.1 状態の所有はアプリ / サーバー側に寄せる
React は、アプリケーションロジックやデータの管理までを担う"フレームワーク"ではありません。その役割は一貫して UI の宣言的な描画にあります。
そのため、React が目指す未来では、
- 状態はアプリケーション内部のロジック(外部ストア)
- さらに大きな視点ではサーバー(RSC の世界)
- そして React はその「状態を読むだけ」の存在
という役割分担がますます明確になります。
状態は React の外にあるべき理由
- データフェッチやビジネスロジックは UI より長い寿命を持つ
- 並列レンダリング(Concurrent Rendering)では UI と状態が結合していると破綻する
- Server Components によってデータはサーバー側で決まる世界が浸透しつつある
- React は UI そのものに集中し、アプリロジックは外側に委ねる方がスケールする
React が状態を「持つ」のではなく、外部ストア / サーバーが状態を提供し、React がその最新スナップショットを描画する。 この役割分離が、今まさに React の中心に戻りつつあります。
8.2 ライブラリの時代から標準 API の時代へ
React の歴史を振り返ると、状態管理は長らく「ライブラリの領域」でした。
- props drilling の課題を埋めた Flux / Redux
- 派生状態の最適化を可能にした Recoil
- Just JavaScript の哲学で広まった Zustand / jotai
- さらに Valtio、MobX など多様なアプローチが並列に発展
しかしこれらは、ある意味で React 本体の足りない部分を補う拡張でもありました。
React チームはこうした膨大な知見を観察し、その本質を標準 APIに落とし込んだのが useSyncExternalStore です。
8.3 useSyncExternalStore は"決着"ではない、スタートラインである
useSyncExternalStore は次のような意味を持ちます。
- 外部ストアと React を同期する"正しい低レイヤ API"を提供
- ライブラリ作者が安全に作るための共通基盤を整備
- React 自体の進化(Concurrent / Server Components)との整合性を保証
これにより、今後の状態管理ライブラリは React の内部仕様の変化に左右されにくい安定した基盤の上で進化できるようになります。
8.4 React と矛盾しない状態管理が今後のスタンダードに
状態管理ライブラリは今後も多様に存在し続けるでしょう。しかしその根底には、
- React は状態を所有しない
- React は状態を読むだけ
- 状態の管理は React の"外側"に置かれる
という大原則が流れ続けます。
この世界観に矛盾しないライブラリだけが、今後の React の進化(Server Components、並列レンダリング、境界の最適化…)と比較的自然に共存しやすいと考えています。
そして useSyncExternalStore は、まさにそのための共通の"言語"でありインターフェースなのです。
9. まとめ
React の状態管理の歴史を振り返ると、単に「便利なライブラリが増えた」という話ではなく、React が UI をどう捉えてきたかという思想の変遷そのものだったことが分かります。
- Redux は「状態の正しさ」と「予測可能性」をもたらし、SPA 開発の基礎を築いた
- Recoil は「依存関係を持った状態」を宣言的に扱うという React 的アプローチを推し進めた
- Zustand は「Just JavaScript」という軽量さと柔軟性で、多くの現場ニーズに応えた
- useSyncExternalStore はそれらすべてを土台に、React が公式に"外部ストアとの同期"を定義した
特に React 18 以降の世界では、React は状態を所有しない。React は UI を描くためのライブラリであり、状態は外部(アプリロジック / サーバー)が保持する。 という方向性がより明確になりました。
ここで重要なのは、状態が UI と分離されるからといって、関係が薄れるわけではないという点です。
状態は UI と切り離すのではなく、「同期」される対象になる
React チームの発言や設計方針を見ると、少なくとも私は React が目指しているのは、UI から状態管理を切り離すこと——しかし UI と状態がバラバラに動くのを許容することではないと考えます。
むしろ React 18 の世界では、状態は外部に置かれ、React はその状態を常に"同期"し続ける存在になると考えた方が正確です。
UI = f(state) の原則はそのままに、「状態がどこにあるか」ではなく**"どう同期されるか"**が重要なテーマへと移っていきました。
そのために導入されたのが useSyncExternalStore であり、これは UI と状態を破綻なく同期し続けるための公式インターフェースです。
- UI は状態の最新スナップショットを常に描き続ける
- 状態側が変われば React に正しく通知される
- 並列レンダリングでもズレが生まれない
この「同期」を軸とした設計こそ、React が次の10年に向けて定義した新しい状態管理の基盤です。
状態管理に"正解"はない。しかし React と矛盾しない設計が求められる
過去10年でさまざまなライブラリが登場したように、状態管理にはユースケースによって向き不向きがあります。
- 大規模・厳格 → Redux
- 依存関係や派生状態中心 → Recoil
- 小〜中規模の柔軟性 → Zustand / jotai / Valtio
- 公式との完全整合 → useSyncExternalStore ベースの外部ストア
しかしどれを選ぶにしても重要なのは、React の思想(状態は外側にあり UI はその"同期された投影"である)と矛盾しないか? という視点です。
最後に
React の状態管理の進化は、常に React 自身の進化と共にありました。そして今、React は"UI = 状態のスナップショット"という原点に立ち返りながら、Concurrent Rendering や Server Components という新しい時代へ踏み出しています。
この記事が、過去10年の状態管理をただ振り返るだけでなく、React がこれから向かう方向を理解する手がかりになれば幸いです。
スライド作成と記事執筆にあたって、Redux開発当初のドキュメントや当時の議論を読み返しました。
開発者インタビューにもあたりました。驚いたのは、Reduxの誕生経緯を語るインタビューが、今読んでも全然古く感じないことです。
普段のフロントエンド開発では、複数のライブラリをパッケージマネージャー経由で組み合わせています。ライブラリを選定するとき、大事にするポイントは人それぞれあると思います。ただ、やりたいことをスマートに実現してくれるなら、それ以上深く踏み込むことは少ないのではないでしょうか。
フロントエンドはずっと揺り戻しの中にいます。いくつものライブラリが生まれ、役目を終え、消えていきました。この流れの激しさ、次のデファクトを狙う開発者たちの熱意を間近に感じられるのが、この領域の面白さだと思います。
今回は状態管理と、そこに解決策を提示してきたライブラリの歴史を振り返りました。得られたのは単なる歴史ではなく、先人たちが残してくれた思考の過程です。
今後も楽しく開発を続けていくために、「そもそもこれは何だったのか?」「どんな思考でこのライブラリは作られたのか?」そういう視点を、自分のライブラリ選びや開発の軸に加えていきたいと思いました。
参考資料
React 公式ドキュメント
Flux アーキテクチャ
- Flux 公式ドキュメント
- In-Depth Overview
- GitHub: facebookarchive/flux
- Facebook: MVC Does Not Scale, Use Flux Instead(InfoQ, 2014)
Redux
一次資料(Redux の誕生)
- Initial commit(2015年5月29日) — Redux リポジトリの最初のコミット
- Live React: Hot Reloading with Time Travel - React Europe 2015(YouTube) — Dan Abramov が Redux を生み出すきっかけとなった発表
- Redux - Reinventing Flux - Interview with Dan Abramov(SurviveJS) — Redux 誕生の経緯を本人が語るインタビュー
- Dan Abramov, co-author of Redux(egghead.io ポッドキャスト) — Redux の成功と開発背景についてのインタビュー
公式ドキュメント・解説
- Redux 公式サイト
- The History of Redux(公式)
- Motivation(公式)
- You Might Not Need Redux(Dan Abramov, 2016)
- Idiomatic Redux: The Tao of Redux, Part 1
Recoil
Zustand
useSyncExternalStore / React 18
- useSyncExternalStore(React 公式)
- useMutableSource → useSyncExternalStore(React 18 Working Group)
- use-sync-external-store(npm パッケージ)
- useSyncExternalStore: Demystified(Epic React)
- React 18 milestone: React-Redux adopts useSyncExternalStore
Discussion