本記事について
当サイトを閲覧いただきありがとうございます。 本記事はシリーズ『生成AI時代のアーキテクチャ超入門』の「フロントエンドアーキテクチャ」カテゴリ第3弾として、状態管理について解説する記事です。
モダンフロントエンドで最も設計が難しい領域。本記事では状態の5種類(UI・ドメイン・サーバ・URL・永続)の分類、Zustand・TanStack Query・React Hook Form + Zodといった現代定番スタック、設計原則、そして「同じ事実を2箇所に書かない」という鉄則を解説します。
本記事のテーマについてさらに詳しく知りたい方は『アプリケーションアーキテクチャ設計パターン』も参考にしてみてください。
そもそも状態管理とは何か
状態管理とは、ざっくり言えば「画面に表示されるデータや操作の状態を、アプリのどこで持ってどう更新するかを決めること」です。
ホワイトボードの情報共有を想像してください。小さなチーム(小規模アプリ)なら1枚のホワイトボード(useState)で全員が情報を確認できます。しかし部門が増えると、各部門に専用ボード(Zustand)を置き、全社掲示板(サーバ状態=TanStack Query)から最新情報を取得する仕組みが必要です。どの情報をどこに書くかのルールがないと、古い情報を見て判断する人が出てきます。
なぜ状態管理の設計が重要なのか
もし状態管理を曖昧にしたらどうなるか。「この値どこで管理してるの?」「更新したのに反映されない」といった混乱は、状態管理設計の失敗がほぼ原因です。アプリの規模が大きくなるほど露骨にコード品質に現れ、一度こじれると新規で書き直すより大変な領域です。
状態は種類ごとに最適な管理方法が違います。一括りに扱うと必ず破綻します。
状態の種類
まず重要なのが、「状態」と一口に言っても性質の全く違う5種類が存在するという認識です。これらを同じ仕組み(Redux等)で管理しようとすると必ず破綻します。
| 種類 | 例 |
|---|---|
| UI状態 | モーダル開閉・入力値・ローディング表示 |
| ドメイン状態 | カート内容・お気に入りリスト |
| サーバ状態 | APIから取得したユーザー一覧・商品詳細 |
| URL状態 | クエリパラメータ・ページネーション・検索条件 |
| 永続状態 | ログイン情報・テーマ設定・下書き |
特に「サーバ状態とUI状態を混ぜる」のが典型的な失敗で、この2つはキャッシュが必要か・いつ古くなるかといった根本的な性質が異なります。状態設計の第一歩は、扱うデータがどの種類に属するかを見極めることです。
現代の常識は「サーバ状態はTanStack Query、UI状態はuseState / Zustand」と分けて管理することです。
ローカル状態とLift State Up
最もシンプルで最も多く使うのがローカル状態です。1つのコンポーネント内でしか使わない値なら、useState で持つのが最もシンプルで問題が起きません。
const [count, setCount] = useState(0)
const [isOpen, setIsOpen] = useState(false)
重要な原則は「複数コンポーネントで使いたくなってから上に持ち上げる」ことを考えれば十分、ということです。最初からグローバル状態に入れる必要はありません。「必要になる前に抽象化するのは設計の失敗」につながります。
状態を複数のコンポーネントで共有したくなったら、まずは共通の親コンポーネントに持ち上げるのが基本です。これを Lift State Up と呼びます。
[Parent] ← useState でここに持つ
├─ Child1 ← propsで受け取る
└─ Child2 ← propsで受け取る(setterも渡す)
この方法は React 公式の鉄板パターンで、シンプルで明示的な利点があります。ただし階層が深くなってくると、中間のコンポーネントを props がバケツリレーのように通過する「Prop Drilling」(propの垂れ流し)が起きます。4〜5層以上になったら、Context または外部Storeの導入を検討するタイミングです。
シンプルな値は useState で十分。早すぎる抽象化こそ最大の敵です。
グローバル状態の選択肢
アプリ全体で共有する状態(ログインユーザー情報・テーマ・サイドバーの開閉等)には、グローバルストアを使います。選択肢は複数あり、チームの好みとアプリ規模で選びます。
| ライブラリ | 特徴 |
|---|---|
| Redux / Redux Toolkit | 古参・エコシステム巨大・ボイラープレート多 |
| Zustand | 軽量・フック主導・書き心地がシンプル |
| Jotai | アトム指向・React並行モード対応 |
| Recoil | Facebook発だがメンテ停滞中・選ばない方が無難 |
| Valtio | Proxyベース・書き味が自然 |
| MobX | Observable主導・Vue的な感覚 |
新規プロジェクトなら Zustand または Jotai が本命です。Redux は複雑な大規模SPAや、Redux DevTools のデバッグ力を活かしたいケースに限定。Reduxの「アクション → リデューサー → ストア」の図式は概念的には美しいですが、「記述量の多さ」が嫌われる傾向にあります。
新規採用は Zustand / Jotai。Reduxを選ぶなら明確な理由を持って選びます。
React Contextの注意
React標準の Context API は、グローバルストアの軽量版として使えます。ただし重大な制約があり、Contextの値が1つでも変わると、購読している全コンポーネントが再レンダリングされるという仕様があります。
❌ ContextにフォームState全体を突っ込む
→ 毎キー入力で全購読コンポーネントが再描画(パフォーマンス壊滅)
✅ Contextは変化頻度の低い情報に限定
→ テーマ / 認証情報 / i18n(言語設定)等
このため、「頻繁に変わる値をContextに入れるのはアンチパターン」です。フォームの入力値やカウンタなどを Context に乗せてはいけません。一方、テーマ・認証状態・i18n のような「稀にしか変わらない値」には最適です。
サーバ状態は特別
APIから取得したデータ(サーバ状態)は、UI状態とは根本的に性質が違うため、同じ仕組みで扱ってはいけません。これは現代フロントエンド設計における最重要の原則です。
| 特徴 | UI状態 | サーバ状態 |
|---|---|---|
| 真実の出所 | ローカル | サーバ |
| 古くなるか | ならない | なる(再取得が必要) |
| 同期の必要 | なし | あり(キャッシュ制御) |
| 複数コンポーネントで必要 | 時々 | 頻繁に |
これを理解していないと、Reduxに取得したユーザーリストを突っ込んで「いつリフレッシュするか」を手書きで実装し、キャッシュバグと古いデータ表示に悩まされる、という古典的な苦行に陥ります。サーバ状態には専用ライブラリを使うのが本命です。
サーバ状態のライブラリ
サーバ状態を扱う専用ライブラリは、キャッシュ管理・再取得・楽観更新といった面倒な処理を全て肩代わりしてくれます。現代のデファクトは TanStack Query(旧React Query)です。
| ライブラリ | 特徴 |
|---|---|
| TanStack Query(React Query) | デファクト。キャッシュ・再取得・楽観更新の全部入り |
| SWR | Vercel製。より軽量・シンプル |
| Apollo Client | GraphQL専用 |
| RTK Query | Redux Toolkit統合版 |
const { data, isLoading, error } = useQuery({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
staleTime: 60_000, // 60秒間は再取得しない
})
queryKeyでキャッシュを管理(同じキーなら同じキャッシュを共有)staleTimeで再取得の頻度を制御invalidateQueriesで Mutation 後にキャッシュを無効化して自動再取得
この3つの仕組みだけで、大半の状態管理問題が解決します。
TanStack Query が既定の第一選択です。全部入りで学習コストも妥当です。
URL状態
ページネーション・検索条件・タブ選択のような情報は、URLに持たせるのが現代的なベストプラクティスです。Reactのステートに持つとブラウザバックやシェア時に情報が失われます。
❌ setState({ page: 2, search: "foo" })
→ ブックマーク不可・リロードで消える
✅ router.push('?page=2&search=foo')
→ ブックマーク可・シェア可・バック対応
URL状態のメリットは以下の通りです。
- ブックマーク可能(その状態を保存できる)
- ブラウザバック/フォワードに対応(履歴が自然に機能)
- シェア可能(URLを送るだけで同じ画面を再現)
- サーバが状態を知れる(SSR時にデータを返せる)
「画面遷移で消えても困らない値以外は、URLに入れる」が鉄則です。
フォーム状態
ユーザー入力を扱うフォームは、専用ライブラリを使うと実装が格段に楽になります。入力数が10個を超えるフォームで useState を使い続けると、管理が破綻します。
| ライブラリ | 特徴 |
|---|---|
| React Hook Form | 非制御・高速・デファクト |
| Formik | 制御式・旧来派・やや重い |
| TanStack Form | 新興・型システムが強力 |
| Zod(バリデーション) | スキーマ駆動のバリデーション決定版 |
「非制御(uncontrolled)方式」は、入力のたびにReactの状態を更新しない設計で、大きなフォームでも再レンダリングがほぼ発生しないため高速です。現代は React Hook Form + Zod がフォーム実装の事実上の標準です。
制御式は10個程度の小さなフォームまで。それを超えたら React Hook Form 一択 です。
永続化
アプリを閉じても残したい情報は、ブラウザの永続ストレージに保存します。何を何に入れるかには明確な使い分けがあり、セキュリティに直結します。
| 対象 | 保存先 | 理由 |
|---|---|---|
| 認証情報 | httpOnly Cookie(推奨) | XSS(Cross-Site Scripting、スクリプト注入攻撃)で盗めない |
| テーマ設定 | localStorage | 機密性低い設定値 |
| 下書き保存 | localStorage / indexedDB | サイズによる |
| セッションキャッシュ | sessionStorage | タブを閉じると消える |
| 大量データ | indexedDB | MB単位の格納も可 |
JWT(署名付き認証トークン)やセッションIDを localStorage に入れるのはXSS脆弱性の王道パターン。JavaScriptから読めるストレージは攻撃者からも読めるため、認証情報は必ず httpOnly Cookie に入れます。
認証情報はhttpOnly Cookie。localStorageに入れるのは地雷です。
「同じ事実を2箇所に書いた日」(業界事例)
フロント開発では、「items 配列と別に itemCount という数値ステートを作ってしまい、削除処理でどちらか片方だけ更新するバグに何度も遭遇した」という話をよく聞きます。items.length をその場で計算するだけで避けられるのに、ついつい「カウントは事前に持っておいた方が速そう」「他の場所でも使うから」と別ステートに切り出してしまう。この種の失敗は新人からベテランまで経験する、状態設計の王道の地雷です。
React学びたての頃に似たバグを起こし、レビューで「items.lengthでいいよね」と指摘されて頭を抱えた、という体験談もよく聞きます。教訓はシンプルで、「同じ事実を2箇所に書くと必ず片方が嘘になる」ということ。
派生状態は計算で導出する、URLで表せるものはURLに入れる、サーバ状態はサーバに事実を預ける。「状態を最小限に保つ」という原則は、抽象的な美学ではなく、過去のバグから学んだ具体的な防御策です。
状態管理の鉄則は「単一の真実の出所」これ一つでバグが激減します。
状態設計の原則
5種類の状態をうまく管理するための設計原則をまとめます。どれも大切ですが、特に「単一の真実の出所」と「派生状態は計算で導出」は、どの規模のアプリでも守るべき鉄則です。
- 単一の真実の出所(Single Source of Truth):同じ情報を2箇所に持たない。必ず片方が主でもう一方は従にする
- 派生状態は計算で導出:
items.length === 0を別のステートで持たず、その場で計算する - サーバとクライアント状態を混ぜない:TanStack Query と Zustand を使い分ける
- 状態を最小限に保つ:計算できるものは保持しない(バグの温床になる)
- URLで表現できるものはURLに:ページネーションや検索条件
規模別の推奨スタック
「全部Reduxに入れる」が破綻の元。プロジェクト規模と状態種別で複数ライブラリを組み合わせるのが現代の鉄板です。
| プロジェクト規模 | UI状態 | サーバ状態 | フォーム | 永続化 |
|---|---|---|---|---|
| 個人・MVP(〜1000行) | useState | fetch直叩き + useState | useState | localStorage |
| 初期スタートアップ(〜5000行) | useState + Context | TanStack Query | React Hook Form + Zod | localStorage |
| 中規模SaaS(〜30,000行) | Zustand | TanStack Query | RHF + Zod | localStorage + httpOnly Cookie |
| 大規模SPA(30,000行〜) | Zustand + Context | TanStack Query | RHF + Zod | Cookie中心 |
| Next.js App Router | Zustand(Client)+ RSC(Server) | Server Componentsで取得 | Server Actions + Zod | httpOnly Cookie |
「Reduxを新規採用する意味は2026年時点ではほぼない」が業界の共通認識です。Redux Toolkitを残しているのは、Redux DevToolsの時間旅行デバッグを活用する大規模プロジェクトか、既存Redux資産を抱えるチームだけです。Jotai / Valtio / Signalも選択肢ですが、情報量・採用事例でZustandが頭ひとつ抜けているのが現状です。
新規採用は Zustand + TanStack Query + RHF + Zod。Reduxを選ぶ理由は狭いです。
やってはいけないこと
状態設計で事故る典型を整理します。どれも無限ループ・二重更新・XSS漏洩の直接原因になります。
| 禁じ手 | なぜダメか |
|---|---|
| JWTをlocalStorageに保存 | XSS一発で全員分漏洩の地雷。httpOnly Cookie必須 |
| サーバ状態をReduxに入れてキャッシュロジック自作 | 古い設計。TanStack Query / SWR で自動化できる |
| 同じ事実を2箇所に持つ(items配列とitemCount別ステート) | 片方が必ず嘘になる。派生状態は計算で導出 |
| React Contextに頻繁に変わる値を入れる | 購読コンポーネント全てが再描画。パフォーマンス壊滅 |
| 早すぎるグローバル化(useStateで足りるのにZustand) | オーバーヘッドだけで恩恵なし。必要になってから |
| フォームの入力値をContextで管理 | キー入力ごとに全再描画。React Hook Formで解決 |
| ブラウザバック / リロードで消える値をステートに持つ | URLに入れればシェア・ブックマーク可能 |
| ページネーション・検索条件をuseState | URLクエリに入れるのが鉄則。リロードで消える設計はUX劣悪 |
| useEffect でデータ取得を手書き | ローディング・エラー・競合状態の地雷。TanStack Query で解決 |
| Refresh Tokenをlocalstorage | XSSで全端末セッション奪取可能。httpOnly Cookie必須 |
| 「Reduxに全部入れる」設計 | サーバ状態とUI状態を混ぜてキャッシュ自作で破綻。種類で道具を分ける |
| 「ContextはReduxの代替」と誤解 | 頻繁に変わる値で全購読コンポーネント再描画。パフォーマンス壊滅 |
「useStateからの段階的な拡張」が健全なパスです。最初からReduxやJotaiを入れるのは典型的な過剰設計で、Context → Zustand → Redux Toolkit の順で必要に応じて昇格させます。
早すぎる抽象化は最大の敵。状態管理は必要になってから拡張します。
AI判断軸
| AI時代に有利 | AI時代に不利 |
|---|---|
| Zustand(フック主導・シンプル) | Reduxの古典的3ファイル分割 |
| TanStack Query + React Hook Form + Zod | 自作キャッシュ・独自バリデーション |
| Server Components + Server Actions | クライアントで全部フェッチ |
| URL状態(searchParams活用) | ステートに詰め込む設計 |
- 状態の種類で道具を分ける(UI/ドメイン/サーバ/URL/永続)
- サーバ状態は専用ライブラリ(TanStack Query一択)
- 主流スタックに寄せる(Zustand + TanStack Query + RHF + Zod)
- スキーマ駆動(Zodで型を共通言語に)
AIが状態管理で間違えやすいパターン
AIにReactの状態管理を書かせると、以下の問題が起きやすいです。
- サーバ状態を
useStateで管理してキャッシュ・再取得・楽観的更新が抜ける(TanStack Queryを使うべき箇所) - グローバルstateに何でも入れて再レンダリングが多発する
- URLに含めるべきフィルタ条件をステートに閉じ込めてブックマーク不可にする
Zodスキーマでデータの型を明示し、TanStack Queryでサーバ状態を分離する設計を先に決めておけば、AIはその制約の中で正確なコードを書きます。
Zodスキーマが型安全とAI精度の両方を支える
Zodで定義したスキーマは、フォームバリデーション・API レスポンスの型チェック・テストデータの生成に一元的に使えます。AIにとっても、Zodスキーマが存在すればデータの形状が明確なため、正しい型のコードを生成する精度が上がります。スキーマなしの動的データ構造では、AIが推測に頼って型エラーを含むコードを出しがちです。
決めるべきこと — 自分のプロジェクトでの答えは?
以下の項目について、自分のプロジェクトの答えを1〜2文で言語化してみてください。曖昧なまま着手すると、必ず後から「なぜそう決めたんだっけ」が問われます。
- グローバル状態ライブラリの採否と選定(Zustand / Jotai / Redux)
- サーバ状態ライブラリの選定(TanStack Query / SWR)
- フォームライブラリとバリデーション(React Hook Form + Zod)
- 永続化戦略(Cookie / localStorage の使い分け)
- URL状態の利用範囲
- 型定義の共有方法(Zod / tRPC / OpenAPI)
この記事に関連する記事
https://senkohome.com/arch-intro-frontend-bff/ https://senkohome.com/arch-intro-frontend-framework/ https://senkohome.com/arch-intro-frontend-overview/
まとめ
本記事は状態管理について、5種類の状態分類・主流スタック・URL状態・永続化まで含めて解説しました。如何だったでしょうか。
状態を最小限に保ち、種類で道具を分け、主流スタックとスキーマ駆動に寄せる。これが2026年の状態管理の現実解です。
次回はフレームワーク詳細(React/Vue/Svelte/Next.js/Astro)について解説します。
シリーズ目次に戻る → 『生成AI時代のアーキテクチャ超入門』の歩き方
本記事で扱った内容の詳細は Redux 公式ドキュメント も合わせて参考にしてください。
それでは次の記事も閲覧いただけると幸いです。
📚 シリーズ:生成AI時代のアーキテクチャ超入門(33/89)
