Skip to content
Published on

状態管理ルネサンス 2025 — Zustand·Jotai·Valtio·TanStack Query·Signals·XState·RSC (シーズン6 第10回)

Authors

プロローグ — 状態管理ルネサンス

「どの状態管理ライブラリ?」は2018年の面接質問だった。2021年には皆がReduxボイラープレートを嫌い、2024年にはReduxToolkitをデフォルトにする人もいなくなった — 質問は**どの状態をどこに?**に分裂した。

今日、状態は明確に分かれた家に住む:

  • サーバ状態 → TanStack Query / SWR / Apollo。
  • URL状態 → ルータパラメータ、検索パラメータ。
  • Server Components → 状態がクライアントに届かない。
  • クライアントUI状態 → Zustand / Jotai / Valtio / React Context。
  • フォーム状態 → React Hook Form / TanStack Form。
  • 状態機械 → XState / Robot / tiny-fsm。
  • 派生/リアクティブ → Signals (Preact、Solid、Angular、React提案)。

本稿は各々をマップ、どれに手を伸ばすか、よくあるアンチパターンを説明する。


1章. 状態の4カテゴリ

  1. サーバ状態 — バックエンドが所有。ユーザー情報、投稿、商品。キャッシュであり状態でない。
  2. URL状態 — URLがすでにエンコード。現在のタブ、ソート順、検索クエリ。
  3. UI状態 — 純粋にクライアント側のプレゼンテーション。メニュー開閉、モーダル、テーマ。
  4. フォーム状態 — 送信前の一時編集状態。

バグのほとんどは状態を間違ったカテゴリに置くことから来る。Reduxのサーバデータ = 古いバグ。ローカル状態のURL状態 = 壊れた戻るボタン。


2章. サーバ状態 — TanStack Queryが勝利

TanStack Query (旧 React Query) がサーバ状態カテゴリを食った。理由:

  • キャッシュ、重複排除、バックグラウンド再取得、stale-while-revalidate — 無料。
  • ミューテーション + 楽観的更新。
  • 無限クエリ、ページネーションヘルパ。
  • fetch、axios、GraphQL、tRPC、Promiseを返すものなら何でも動作。

最小:

const { data, isLoading } = useQuery({
  queryKey: ['posts', postId],
  queryFn: () => fetch(`/api/posts/${postId}`).then(r => r.json()),
})

競合

  • SWR — 軽め、類似アイデア。シンプルアプリに良。
  • Apollo Client — GraphQLを使っているなら。
  • RTK Query — Redux Toolkitの一部、既にそこにいるチーム向け。

ルール: サーバデータを決してRedux/Zustandに入れない。TanStack Queryを使う。


3章. クライアントUI状態 — Zustand, Jotai, Valtio

Zustand

グローバルストア、最小API、ボイラープレートなし。

import { create } from 'zustand'
const useStore = create((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
}))

強み: 小 (~1kb)、どこでも動作、Redux DevTools統合、immerミドルウェア。 用途: グローバルUI状態 (テーマ、モーダル、サイドバー)。シンプルアプリ。

Jotai

アトムベース。各状態がアトム、コンポーネントがアトムに購読。

import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const [count, setCount] = useAtom(countAtom)

強み: 細粒度リアクティブ、複雑な派生状態に良、ミドルウェア豊富。 用途: 小さな独立ピース多数、派生アトム、永続化/同期ニーズ。

Valtio

プロキシベース。状態をmutateすると再レンダー。

import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0 })
state.count++ // 購読コンポーネントで再レンダートリガ

強み: 命令的ミューテーションに最も自然なAPI。キャンバス/ホワイトボードアプリに良。 用途: 多くのミューテーションポイントを持つ複雑なネスト状態。

選び方

  • 90%のアプリ → Zustand。
  • 複雑な派生状態 → Jotai。
  • 命令的ミューテーション重 (エディタ、ホワイトボード) → Valtio。
  • エンタープライズ一貫性 → Redux Toolkit (今も最も安全な「チームオンボード」選択)。

4章. Signals — リアクティブ革命

SignalsはSolidが普及させた細粒度リアクティブプリミティブ。Preact、Svelte 5 (runes)、Angular、Vue (refs) が採用、TC39標準トラック提案。

// Preact Signals
import { signal, computed } from '@preact/signals'
const count = signal(0)
const doubled = computed(() => count.value * 2)
// 更新: count.value++

なぜSignalsが重要か:

  • 再レンダーカスケードなし — signalを読むDOMノードのみ更新。
  • 依存配列なし — 依存グラフが暗黙。
  • 合成可能computedがReactのuseMemo体操なしで派生状態を与える。

Reactの状況 (2026): TC39 Signals提案はStage 1。React自体はsignalsを採用せず、Reactチームはコンパイラベース自動メモ化 (React Compiler、2025 RC) を好む。今のところReactのsignalsはライブラリ経由 (@preact/signals-react@preact/signals-react-runtime、またはsignal風アトムとしてJotai)。


5章. 状態機械 — XState

XStateは明示的な状態、遷移、ガードを持つアクターとして状態をモデル化。

import { createMachine } from 'xstate'
const fetchMachine = createMachine({
  id: 'fetch',
  initial: 'idle',
  states: {
    idle: { on: { FETCH: 'loading' } },
    loading: { on: { SUCCESS: 'done', ERROR: 'error' } },
    done: {},
    error: { on: { RETRY: 'loading' } },
  },
})

用途

  • 複雑なUIフロー (複数ステップフォーム、ウィザード、オンボーディング)。
  • 多くの状態を持つステータス (「isLoading / error / data」だけでない)。
  • プロダクトマネージャ用の視覚状態図が必要。

使わない時

  • シンプルなon/offフラグ。
  • 完全にTanStack Queryが管理する状態。

XState v5 (2024) はTypeScriptエルゴノミクス、アクターモデル、小バンドルに注力。RobotとXState/fsmはより小さな代替。


6章. フォーム状態 — React Hook Form / TanStack Form

React Hook Form (RHF)

2020–2024のデフォルト。非制御入力、resolvers経由Zod統合、性能良好。

TanStack Form (2024+)

フレームワーク非依存、ヘッドレス。複雑フォームに豊か、TS推論良好。

どちらに手を伸ばすか

  • 単一入力より長いフォームなら何でも。useStateからフォーム状態を作らない。
  • スキーマ + 検証にZodまたはValibot。

7章. URL状態 — useSearchParams, nuqs

  • Next.js useSearchParams + Server Component読み取り。
  • nuqs — 型付き検索パラメータ、変更時URLに同期。
  • @tanstack/router — 第一級検索パラメータを持つ型付きルータ。

ルール: ビューを変え、リンクで共有可能ならURL状態にすべし。タブ、フィルタ、ソート順、ページネーション。


8章. React Context — 誤解されたツール

Contextは依存注入機構であり、状態ライブラリでない。使う時:

  • 変更頻度の低い依存 (テーマ、認証ユーザー、i18n)。
  • propsドリルなしで物を下に渡す。

使わない時:

  • 頻繁に変わる状態 (全コンシューマ再レンダー)。
  • Zustand/Jotaiの置換として。

9章. React Server Components — 状態でない状態

RSCは巨大クラスの問題のクライアント状態を排除。データがサーバから来てクライアントで変わらないなら、状態管理を必要としない。

ルール: ストアに追加する前に「これはRSCにできるか?」と問う。最高の状態は状態なし。


10章. 永続化 — localStorage, IndexedDB, cookies

  • localStorage — 同期、小、テーマ/設定用。
  • IndexedDB (Dexieまたはidb-keyval経由) — 大、非同期、オフラインデータ用。
  • Cookies — サーバ可読状態 (認証、SSRが必要な設定)。

Zustandにpersistミドルウェア、JotaiにatomWithStorage、ValtioにproxyWithHistory


11章. デバッグとDevTools

  • Redux DevToolsがZustand、Jotai、Valtioと統合 — タイムトラベル、アクションログ。
  • TanStack Query DevTools — キャッシュインスペクタ、クエリごとステータス。
  • XState Inspector / @xstate/inspect — ライブ状態機械ビューア。
  • React DevTools Profiler — レンダー追跡。

ルール: 状態解にタイムトラベルや状態ツリービューがないなら、後で代償を払う。


12章. 2026年のスタック

新Reactアプリの実用的デフォルト:

  • サーバ状態: TanStack Query。
  • クライアントUI状態: Zustand (1つのストア)。
  • フォーム: React Hook Form + Zod。
  • URL状態: nuqs (またはルータ内蔵)。
  • 状態機械: 複雑さが正当化する時のみXState。
  • 派生リアクティブ: React Compiler (2025 RC) またはJotaiアトム。
  • RSCでサーバ専用データ

13章. 2026年以降の注視点

  1. React Compiler安定 — 自動メモ化が「性能」仕事を変える。useMemo/useCallbackが稀に。
  2. Signalsがweb標準 — TC39提案進行でブラウザネイティブプリミティブ。
  3. フレームワーク横断RSC — SolidStart、Remix (React Routerとの合併後)、AstroがRSC風境界を採用。
  4. アクターモデル標準化 — XState v5アクターパターンがメインストリームへ。
  5. Syncエンジン — Replicache、Zero (Replicacheチーム)、Liveblocksがサーバとクライアント状態の境界を曖昧に。「Local-first」上昇。

12項目チェックリスト

  1. サーバ、URL、クライアントUI、フォーム状態を分離?
  2. サーバデータはTanStack Query (またはSWR/Apollo) で、Reduxでない?
  3. URL状態が実際のURLに反映?
  4. ContextはDI用、頻繁変更状態でない?
  5. デフォルトクライアントストアが1つ (Zustandまたは同等)?
  6. フォームはRHF/TanStack Form + Zod使用?
  7. 複雑フローをXStateでモデル化?
  8. サーバフェッチUIにRSC使用?
  9. 各状態層にDevTools統合?
  10. 永続化が正しいストレージ (local vs IDB vs cookie)?
  11. useMemo/useCallback肥大を回避 (またはReact Compiler使用)?
  12. オンボード用に状態形状が文書化?

10アンチパターン

  1. Redux/Zustandにサーバデータ — 古いバグ。
  2. ローカル状態にURL状態 — 戻るボタン壊れ、共有不能リンク。
  3. 頻繁変更状態にContext — 再レンダー嵐。
  4. フォームでuseStateカスケード — 代わりにRHF。
  5. ローカルモーダル状態にグローバルストア。
  6. booleanトグルに状態機械。
  7. 複数Zustandストア重複。
  8. useEffectでフェッチ — TanStack Queryを使う。
  9. 「念のため」すべてをlocalStorageに永続化。
  10. 「我々は常にクライアント状態を使った」でRSC回避。

次回予告

シーズン6 第11回: フロントエンドテスト 2025 — Vitest、Jest、Bun test、Testing Library、Playwright、Storybook、MSW、ビジュアル回帰、AI生成テスト。何をどの比率で書き、CIグリーンを保つ方法。

— 状態管理ルネサンス編、完。