本記事について
当サイトを閲覧いただきありがとうございます。 本記事はシリーズ『生成AI時代のアーキテクチャ超入門』の「フロントエンドアーキテクチャ」カテゴリ第3弾として、状態管理について解説する記事です。
モダンフロントエンドで最も設計が難しい領域です。本記事では状態の5種類(UI・ドメイン・サーバ・URL・永続)の分類、Zustand・TanStack Query・React Hook Form+Zodといった現代定番スタック、そして「同じ事実を2箇所に書かない」という鉄則を解説します。
本記事のテーマについてさらに詳しく知りたい方は『アプリケーションアーキテクチャ設計パターン』も参考にしてみてください。
この記事の結論
- 状態は最小限に保つ(計算できるものは持たない)
- サーバー状態はTanStack Query、UI状態はuseState / Zustandと種類で道具を分ける
- 検索条件・ページネーションはURLに持たせる
- 認証情報はhttpOnly Cookieへ。localStorageは地雷
この記事を読む前に
本記事はブラウザ側(画面側)の仕組みの話が中心です。IT用語にあまり馴染みがない方は、基礎編の「Webサービスが動く仕組み」と「プログラムとAPIの基本」を先に読んでおくと格段に分かりやすくなると思います。また、読んでいて分からない用語が出てきたときは用語集で調べながら読み進められます。
そもそも状態管理とは何か
状態管理とは、ざっくり言えば「画面に表示されるデータや操作の状態を、アプリのどこで持ってどう更新するかを決めること」です。
ホワイトボードの情報共有を想像してください。小さなチーム(小規模アプリ)なら1枚のホワイトボード(useState)で全員が情報を確認できます。しかし部門が増えると、各部門に専用ボード(Zustand)を置き、全社掲示板(サーバ状態=TanStack Query)から最新情報を取得する仕組みが必要です。どの情報をどこに書くかのルールがないと、古い情報を見て判断する人が出てきます。
なぜ状態管理の設計が重要なのか
もし状態管理を曖昧にしたらどうなるか。「この値どこで管理してるの?」「更新したのに反映されない」といった混乱は、状態管理設計の失敗がほぼ原因です。アプリの規模が大きくなるほど露骨にコード品質に現れ、一度こじれると新規で書き直すより大変な領域です。状態は種類ごとに最適な管理方法が違い、一括りに扱うと必ず破綻します。
状態の5分類
「状態」と一口に言っても、性質の全く違う5種類が存在します。
| 種類 | 例 |
|---|---|
| UI状態 | モーダル開閉・入力値・ローディング表示 |
| ドメイン状態 | カート内容・お気に入りリスト |
| サーバ状態 | APIから取得したユーザー一覧・商品詳細 |
| URL状態 | クエリパラメータ・ページネーション・検索条件 |
| 永続状態 | ログイン情報・テーマ設定・下書き |
特に「サーバ状態とUI状態を混ぜる」のが典型的な失敗で、この2つはキャッシュが必要か・いつ古くなるかという根本的な性質が異なります。現代の常識は「サーバ状態はTanStack Query、UI状態はuseState / Zustand」と分けて管理することです。
UI状態 ― useStateから段階的に昇格させる
1つのコンポーネント内でしか使わない値なら useState が最もシンプルで問題が起きません。複数コンポーネントで共有したくなったら、まず共通の親に持ち上げる(Lift State Up)のがReact公式の鉄板パターンです。propsのバケツリレー(Prop Drilling)が4〜5層以上になったら、外部Storeの導入を検討するタイミングです。早すぎる抽象化こそ最大の敵で、最初からグローバル状態に入れる必要はありません。
グローバルストアの新規採用はZustand(またはJotai)が本命です。軽量・フック主導で書き心地がシンプル、情報量と採用事例で頭ひとつ抜けています。Reduxを新規採用する意味は2026年時点ではほぼなく、Redux DevToolsの時間旅行デバッグを活かす大規模プロジェクトか既存資産があるチームに限られます。
React標準のContext APIには重大な制約があります。値が1つでも変わると購読している全コンポーネントが再レンダリングされるため、「頻繁に変わる値をContextに入れるのはアンチパターン」です。テーマ・認証状態・i18nのような稀にしか変わらない値に限定します。
サーバ状態 ― TanStack Queryが第一選択
APIから取得したデータは、真実の出所がサーバにあり、時間とともに古くなり、キャッシュ制御が必要──UI状態と根本的に性質が違います。Reduxに突っ込んで「いつリフレッシュするか」を手書きすると、キャッシュバグと古いデータ表示に悩まされる古典的な苦行に陥ります。
現代のデファクトはTanStack Query(旧React Query)です。
const { data, isLoading, error } = useQuery({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
staleTime: 60_000, // 60秒間は再取得しない
})
queryKey でキャッシュを管理し、staleTime で再取得頻度を制御し、Mutation後は invalidateQueries で自動再取得する。この3つの仕組みだけで、キャッシュ・再取得・楽観更新といった面倒な処理の大半が解決します。
URL状態・フォーム・永続化
URL状態:ページネーション・検索条件・タブ選択は、ReactのステートではなくURLに持たせるのが鉄則です。ステートに持つとブラウザバックやリロードで消え、ブックマークもシェアもできません。「画面遷移で消えても困らない値以外は、URLに入れる」です。
フォーム状態:入力数が10個を超えるフォームでuseStateを使い続けると破綻します。現代はReact Hook Form+Zodが事実上の標準で、非制御方式のため大きなフォームでも再レンダリングがほぼ発生しません。
永続化:テーマ設定・下書きは localStorage でよいですが、JWTやセッションIDをlocalStorageに入れるのはXSS脆弱性の王道パターンです。JavaScriptから読めるストレージは攻撃者からも読めるため、認証情報は必ずhttpOnly Cookieに入れます。
どう選べばいいのか ― 規模別の推奨スタック
「全部Reduxに入れる」が破綻の元で、規模と状態種別で複数ライブラリを組み合わせるのが現代の鉄板です。
| プロジェクト規模 | UI状態 | サーバ状態 | フォーム |
|---|---|---|---|
| 個人・MVP | useState | fetch+useState | useState |
| 初期スタートアップ | useState+Context | TanStack Query | RHF+Zod |
| 中規模SaaS〜大規模SPA | Zustand | TanStack Query | RHF+Zod |
| Next.js App Router | Zustand(Client)+RSC(Server) | Server Componentsで取得 | Server Actions+Zod |
useState → Context → Zustand の順で、必要になってから昇格させるのが健全なパスです。
3つのシナリオで考える
個人開発・スタートアップの場合
useStateとTanStack Query、フォームにRHFとZodがあれば十分です。グローバル状態管理ライブラリは最初から入れる必要はなく、Contextで足りなくなってから昇格を検討すれば間に合います。正直、2026年の個人開発で「Reduxから始める」のは過剰装備だと思います。
中小SaaSの場合
UI状態はZustand、サーバ状態はTanStack Query、フォームはRHFとZod、という3点セットが鉄板になります。状態の種別ごとに責務を分けておくことで、「全部Reduxに入れて密結合」という泥沼を避けられるのです。
大企業の場合
大規模SPAになると、状態管理そのものより規約の統一が本題になってきます。Next.jsのApp Routerを使っているなら、RSCやServer Actionsでサーバ状態をサーバ側に寄せて、クライアント状態を最小化する設計が保守コストを下げてくれます。既存のRedux資産がある場合は無理に置き換えず、新規画面から段階的に移行するのが現実的でしょう。
AI判断軸 ― スキーマ駆動がAI精度を支える
AIが状態管理で間違えやすいパターン
AIにReactの状態管理を書かせると、サーバ状態を useState で管理してキャッシュ・再取得が抜ける、グローバルstateに何でも入れて再レンダリングが多発する、URLに含めるべきフィルタ条件をステートに閉じ込める、といった問題が起きやすいです。「サーバ状態はTanStack Query」「検索条件はURL」という設計ルールを先に決めておけば、AIはその制約の中で正確なコードを書きます。
Zodスキーマが型安全とAI精度の両方を支える
Zodで定義したスキーマは、フォームバリデーション・APIレスポンスの型チェック・テストデータの生成に一元的に使えます。AIにとってもZodスキーマが存在すればデータの形状が明確なため、正しい型のコードを生成する精度が上がります。スキーマなしの動的データ構造では、AIが推測に頼って型エラーを含むコードを出しがちです。
やってはいけないこと
無限ループ・二重更新・XSS漏洩の直接原因を、特に危険な6つに絞ります。
| 禁じ手 | なぜダメか → どうするか |
|---|---|
| JWT / Refresh TokenをlocalStorageに保存 | XSS一発で全員分漏洩する → httpOnly Cookieにする |
| サーバ状態をReduxに入れてキャッシュ自作 | 古いデータ表示とキャッシュバグの温床 → TanStack Queryにする |
| 同じ事実を2箇所に持つ(items配列とitemCount) | 片方が必ず嘘になる → 派生状態は計算で導出する |
| Contextに頻繁に変わる値を入れる | 購読コンポーネント全てが再描画される → 稀にしか変わらない値に限定する |
| useEffectでデータ取得を手書き | ローディング・エラー・競合状態の地雷 → TanStack Queryにする |
| ページネーション・検索条件をuseStateに持つ | リロードで消えブックマーク不可 → URLクエリに入れる |
筆者メモ ― 「同じ事実を2箇所に書いた日」
フロント開発では、items 配列と別に itemCount という数値ステートを作ってしまい、削除処理でどちらか片方だけ更新するバグに何度も遭遇した、という話をよく聞きます。items.length をその場で計算するだけで避けられるのに、つい「カウントは事前に持っておいた方が速そう」と別ステートに切り出してしまう。新人からベテランまで経験する、状態設計の王道の地雷です。
教訓はシンプルで、「同じ事実を2箇所に書くと必ず片方が嘘になる」ということ。派生状態は計算で導出し、URLで表せるものはURLに、サーバ状態はサーバに事実を預ける。「単一の真実の出所」という原則は抽象的な美学ではなく、過去のバグから学んだ具体的な防御策です。
決めるべきこと — 自分のプロジェクトでの答えは?
以下の項目について、自分のプロジェクトの答えを1〜2文で言語化してみてください。曖昧なまま着手すると、必ず後から「なぜそう決めたんだっけ」が問われます。
- グローバル状態ライブラリの採否と選定(Zustand / Jotai / Redux)
- サーバ状態ライブラリの選定(TanStack Query / SWR)
- フォームライブラリとバリデーション(React Hook Form+Zod)
- 永続化戦略(Cookie / localStorageの使い分け)
- URL状態の利用範囲
- 型定義の共有方法(Zod / tRPC / OpenAPI)
この記事に関連する記事
まとめ
本記事は状態管理について、5種類の状態分類・主流スタック・URL状態・永続化まで含めて解説しました。如何だったでしょうか。
状態を最小限に保ち、種類で道具を分け、主流スタックとスキーマ駆動に寄せる。これが2026年の状態管理の現実解です。
次回はフレームワーク詳細(React/Vue/Svelte/Next.js/Astro)について解説します。
シリーズ目次に戻る → 『生成AI時代のアーキテクチャ超入門』の歩き方
本記事で扱った内容の詳細は Redux 公式ドキュメント も合わせて参考にしてください。
それでは次の記事も閲覧いただけると幸いです。
📚 シリーズ:生成AI時代のアーキテクチャ超入門(39/95)
