React状態管理の深掘り #1 クライアント状態とサーバー状態の違い
React基礎講座(#1〜#15)では、useStateとuseReducer、状態の引き上げ、useContextまで扱いました。小さなアプリなら、これらの組み込みツールだけで十分です。ところがアプリが大きくなると、useStateとuseContextだけでは手に余る地点が必ず来ます。このシリーズは、まさにその地点から始まります。
全6回で構成します。
- #1 クライアント状態とサーバー状態の違い ← この記事
- #2 TanStack Queryで扱うサーバー状態
- #3 Zustandで扱う軽量なクライアント状態
- #4 Jotaiと原子(atom)モデル
- #5 Redux Toolkitとレガシーの文脈
- #6 どのツールをいつ使うか — 決定ガイド
この記事はコードがほとんどありません。ツールを選ぶ前に、状態には性質の異なる2種類があるという事実を先に整理しておくことが、残り5回を理解するうえで決定的だからです。
組み込みツールが手に余る瞬間 #
Reactが標準で提供する状態ツールは3つです。
useState— コンポーネント1つのローカル状態useReducer— 遷移ルールが複雑なローカル状態useContext— コンポーネントツリーを横断して値を渡す
アプリが大きくなると、これらのツールで2つの異なる問題を同時に解こうとするようになります。そして、その2つの問題は性質がまったく違います。
問題A。 ダークモード設定、サイドバーの開閉、モーダルの表示状態、カートに入った項目のように、ブラウザの中で生まれ、ブラウザの中だけで意味を持つ値を、複数のコンポーネントで共有する必要があります。useContextで解けますが、値が変わるたびにProvider配下の全体が再レンダリングされやすく、Contextが増えるとProviderのネストが深くなります。
問題B。 ユーザー一覧、投稿、注文履歴のように、サーバーのデータベースに本物の原本があり、画面はその複製を見せているだけの値を扱う必要があります。useEffectの中でfetchし、useStateに入れるおなじみのパターンですが、ローディング表示、エラー処理、再取得、キャッシュ、他の画面との同期をすべて手で書いていくと、コードはたちまち複雑になります。
核心はこうです。問題Aと問題Bは同じuseStateで扱いますが、実はまったく種類の異なる状態です。 この2つを区別できないと、ツール選びがずっとずれ続けます。
クライアント状態とサーバー状態 #
状態を次の2種類に分けてみます。
| 区分 | クライアント状態 | サーバー状態 |
|---|---|---|
| 原本のある場所 | ブラウザ(この状態が原本そのもの) | サーバーのDB(画面は複製) |
| 例 | ダークモード、モーダルの開閉、フォーム入力、選択中のタブ | ユーザー一覧、投稿、注文履歴、検索結果 |
| 同期の要否 | 不要(自分のブラウザ内で完結) | 必要(サーバーと合わせ続ける) |
| 鮮度 | 常に最新(自分が変えるので) | 時間が経つと古くなる(他人が変えうる) |
| 主な関心 | 誰が共有しどう更新するか | いつ再取得しどうキャッシュするか |
クライアント状態は、ブラウザの中で生まれ、ブラウザの中で消えます。ダークモードを点けたという事実を、サーバーに尋ねる必要はありません。この状態自体が原本です。
サーバー状態は違います。画面に見えている投稿一覧は本物ではなく、サーバーにある原本の複製です。自分が見ている間に、他のユーザーが投稿を追加したかもしれません。つまりサーバー状態は放っておくと古くなります。 そこから「いつ再取得するか」「取得したものをどれくらいキャッシュするか」「複数の画面が同じデータを見ているとき、どうやって一斉に更新するか」といった関心が付いてきます。クライアント状態にはない関心です。
useEffect + useStateで苦労して書いていたものの正体がはっきりします。なぜこの区別がツール選びを変えるのか #
2つの状態は性質が違うので、よく合うツールも違います。
サーバー状態のためのツールは、キャッシュ、自動再取得、ローディングとエラー状態、バックグラウンド更新を標準で備えているべきです。この領域の標準がTanStack Query(旧名React Query)です。#2で扱います。
クライアント状態のためのツールは、軽くグローバルな値を共有し、必要なコンポーネントだけ再レンダリングされるべきです。この領域には選択肢が複数あります。Zustand(#3)、Jotai(#4)、そして伝統のRedux Toolkit(#5)を順に見ていきます。
ここで最もよくある間違いを先に挙げておきます。サーバーデータを、ReduxやZustandのようなグローバルクライアント状態ストアに丸ごと入れることです。しばらく標準のように使われましたが、こうするとキャッシュ、再取得、鮮度管理をすべて手で作り直すことになります。サーバー状態はサーバー状態ツールに任せるほうが、はるかにシンプルです。
[よくあるアンチパターン]
すべての状態 → 1つの巨大なグローバルストア(Reduxなど)
- ダークモードもここに
- サーバーから受け取った投稿一覧もここに
- キャッシュ・再取得・ローディング処理をすべて手で実装
[推奨構造]
サーバー状態 → TanStack Query(キャッシュ・再取得・鮮度を自動)
クライアント状態 → Zustand / Jotai / Context(軽量なグローバル共有)
ローカル状態 → useState / useReducer(そのまま)ではuseStateはいつ使うのか #
誤解を防ぐために、はっきりさせておきます。このシリーズはuseStateを捨てようという話ではありません。 1つのコンポーネントの中だけで使う状態、たとえばフォームの入力値やトグル1つは、依然としてuseStateが正解です。新しいツールは、次の条件が重なるときに必要になります。
- 複数のコンポーネントが同じ状態を共有する必要があります。
- そのコンポーネントがツリー上で遠く離れていて、propsで渡すのが面倒です。
- (サーバー状態なら)キャッシュと再取得まで気にする必要があります。
これに当てはまらなければ、組み込みツールで十分です。ツールを増やすこと自体がコストだという点を、忘れないでください。
このシリーズで学ぶこと #
各回は、ツール1つを同じ基準で見ていきます。インストール、基本的な使い方、再レンダリングの挙動、どんな状況に合うかを、一貫して比較できるように構成しました。
- #2 TanStack Query — サーバー状態のキャッシュと再取得を自動化
- #3 Zustand — 最小限のボイラープレートでグローバルなクライアント状態
- #4 Jotai — 状態を小さな原子に分割するボトムアップモデル
- #5 Redux Toolkit — 今も多くのコードベースに残る標準の現在の姿
- #6 決定ガイド — 状況別に何を選ぶか
まとめ #
この記事の結論は一文です。状態にはクライアント状態とサーバー状態があり、両者は性質が異なるので、よく合うツールも異なります。
- クライアント状態 — ブラウザの中で完結、それ自体が原本
- サーバー状態 — サーバーに原本がある複製、放っておくと古くなる
新しい画面を作るたびに、自問してください。「この値はブラウザの中で完結するのか、それともサーバーデータの複製なのか?」 この1つの問いが、残りの回でどのツールを取り出すかを決めます。
次回「React状態管理の深掘り #2 TanStack Queryで扱うサーバー状態」では、まずサーバー状態から扱います。useEffect + useStateで書いていたデータ取得が、どうやって数行に縮むのかを実際に見ていきます。