Reactに触れる中で「状態管理」という言葉に出会う。
なんとなく使ってしまっていて、何が状態で、何を管理していて、なぜライブラリが必要になるのかが分かりにくい。
https://www.youtube.com/watch?v=PZY1x4OqQks&t=700s の動画を見て、あらためて学びなおそうと思い、この記事を書くことにした。
まずは、管理する「状態」とは何かに注目して整理してみる。
状態とは何か
状態とは、“その瞬間の画面やアプリのふるまいを決める値”のことである。
たとえば次のようなものである。
- モーダルが開いているかどうか
- 入力フォームに今入っている文字
- ログイン中のユーザー情報
- 商品一覧の取得結果
- 現在選択中のタブ
- カートに入っている商品
逆に、毎回固定で変わらない値は状態ではない。
- サイト名
- 固定メニュー
- 定数の設定値
なぜ状態を管理する必要があるのか
画面というのは、結局のところ状態を見て描画されている。
だから状態が増えるほど、「どこで値を持つか」「誰が更新するか」「どこまで共有するか」を考えないと破綻しやすくなる。
簡単な例で考える。
const [isOpen, setIsOpen] = useState(false);
これは「モーダルが開いているかどうか」を持つ状態である。
このくらいなら単純で、そのコンポーネントの中だけで完結する。
しかしアプリが大きくなると、次のようなことが起こる。
- ヘッダーのボタンでもモーダルを開きたい
- 別コンポーネントからも同じ値を見たい
- API の取得結果を複数画面で共有したい
- 入力内容をページ移動後も保持したい
こうなると、ただ useState を置くだけでは整理しにくくなる。
この「増えてきた状態を破綻しないよう扱うこと」が、状態管理の本質である。
ローカルな状態とグローバルな状態
状態管理を考えるとき、最初に区別したいのがローカルな状態とグローバルな状態である。
ローカルな状態
そのコンポーネントの中だけで閉じている状態。
- 開閉フラグ
- 一時的な入力値
- hover 状態
- UI 上のちょっとした切り替え
これは useState や useReducer で十分なことが多い。
グローバルな状態
複数のコンポーネントや画面で共有したい状態。
- ログインユーザー
- テーマ設定
- カートの中身
- フィルタ条件
- どこからでも参照したい共通 UI 状態
こちらは、props の受け渡しだけではつらくなりやすい。
そのため、Context や専用ライブラリが登場する。
props drilling がつらくなる瞬間
状態管理ライブラリが欲しくなる理由として、よく出てくるのが props drilling である。
これは、必要な値を深い子コンポーネントまで渡すために、途中のコンポーネントが使わない props を延々と中継する状態を指す。
例えばこういう状況である。
- 親がユーザー情報を持っている
- 孫コンポーネントでそれを使いたい
- 間の子コンポーネントは使わないのに props を受け取って渡す
この形が増えると、次の問題が起きる。
- どこから値が来ているか追いにくい
- コンポーネントの責務が曖昧になる
- props が増えて見通しが悪くなる
- 変更時の影響範囲が読みづらい
状態管理ライブラリは、こうした共有状態の置き場を整理するために使われることが多い。
すべてをグローバルにすればよいわけではない
ここでありがちな誤解が、「共有したいなら全部グローバルな状態にすればよいのでは」という考え方である。
しかし実際には、何でも共有に寄せると逆に分かりにくくなる。
例えば次のような状態は、基本的にはローカルに置いた方がよい。
- その場限りのモーダル開閉
- 単一フォームの入力中の値
- アコーディオンの開閉
- タブ切り替え
一方で、次のようなものは共有の価値がある。
- ログイン状態
- 通知状態
- 検索条件の保持
- サイト全体で使う UI 状態
大事なのは、「共有できるか」ではなく「共有すべきか」で判断することだと思う。
状態管理ライブラリは何をしてくれるのか
ライブラリによって思想は違うが、だいたい次のどれかを助けてくれる。
- 状態の置き場所をまとめる
- 複数コンポーネントから同じ状態を読めるようにする
- 更新ロジックを整理する
- どの変更でどこが再描画されるかを扱いやすくする
例えば Redux は「更新ルールを明示的に管理する」方向が強い。
Jotai は「小さな状態を atom 単位で素直に置く」方向が強い。
Zustand は「軽い store を気軽に持つ」感覚に近い。
つまり、状態管理ライブラリは魔法ではなく、状態の散らかり方に対して、整理の仕方を与える道具 である。
React を使わないなら状態管理は不要か
このブログのようにAstroを用いた静的サイト中心の構成では、そもそも複雑なクライアント状態が少ないので、状態管理ライブラリが不要なことは多い。
例えばこのブログなら、
- 記事一覧
- 記事詳細
- タグページ
- アーカイブ
といったページは、ビルド時に内容が決まる。
そのため、React アプリのような大きな状態管理は基本的にいらない。
逆に、次のような機能を入れるなら状態の設計が効いてくる。
- 検索 UI
- フィルタ UI
- ログイン
- コメント
- 下書き保存
- リアルタイム更新
つまり、状態管理は React 専用の難しい話ではなく、「変わる値をどう整理するか」という普遍的な話である。
実務では何を見るか
実務で状態まわりの設計を見るときは、次の点を気にすると整理しやすい。
- その状態は本当に共有が必要か
- ローカルに閉じられないか
- 更新責任はどこにあるか
- サーバー由来のデータなのか、UI 由来のデータなのか
- 再描画や依存関係が分かりやすいか
特に、サーバーから取ってきたデータと UI の一時 state を同じ感覚で扱うと混乱しやすい。
この2つは似て見えて性質が違うので、分けて考えた方がよい。
Jotai はどういうライブラリか
ここまでで、状態管理ライブラリは「共有したい状態の置き場を整理する道具」だと書いた。
その中でも Jotai は、かなり小さく始めやすいタイプのライブラリだと思う。
Jotai では、共有したい状態を atom という単位で定義する。
例えば次のように書ける。
import { atom, useAtom } from "jotai";
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<button onClick={() => setCount((prev) => prev + 1)}>
{count}
</button>
);
}
感覚としては、useState をコンポーネントの外に出して、複数コンポーネントから共有できるようにしたものに近い。
この「useState に近い感覚で使える」というのが、Jotai の分かりやすいところである。
atom とは何か
Jotai の中心にある atom は、状態の最小単位のようなものである。
例えば、
- ログインユーザー
- モーダルの開閉
- 選択中のタブ
- 検索条件
といった状態を、それぞれ別の atom として持てる。
これによって、巨大な store を1つ作って全部押し込むのではなく、必要な状態を必要な単位で分けて持ちやすい。
Redux のように最初から全体構造を大きく設計するというより、必要な状態を少しずつ積み上げる感覚に近い。
Jotai が初学者に分かりやすい理由
状態管理ライブラリで最初につまずきやすいのは、覚える概念が多いことである。
例えば Redux だと、
- store
- action
- reducer
- dispatch
のように、状態を更新するための役割や流れをまとめて覚える必要がある。
一方 Jotai は、最初の入口がかなり素直である。
- atom を作る
useAtomで読む- setter で更新する
と、かなり小さな理解から始められる。
そのため、「props drilling がつらい」「でも大きな状態管理ライブラリはまだ重い」と感じる段階ではかなり相性が良い。
Jotai はどんなときに向いているか
Jotai は、次のようなケースで特に使いやすいと思う。
- 複数コンポーネントで同じ状態を共有したい
- でも Redux ほど大がかりにはしたくない
useStateに近い感覚を保ちたい- 状態を細かく分けて持ちたい
例えば、
- サイト全体のフィルタ条件
- ログインユーザー情報
- モーダルやドロワーの開閉
- 設定画面の入力途中データ
のようなものは、Jotai と相性が良い。
逆に、非常に厳密な更新ルールやイベント履歴の追跡を強く求めるなら、別のライブラリの方が向いていることもある。
useState で十分なものまで Jotai にしない
ただし、Jotai が使いやすいからといって、何でも atom にすればよいわけではない。
例えば次のような状態は、基本的にはそのまま useState でよい。
- そのコンポーネントの中だけで完結する開閉
- 一時的な hover 状態
- 単発の入力欄
大事なのは、Jotai を使うことではなく、「共有したい状態を無理なく共有できること」である。
ローカルで閉じるものまで全部 atom にすると、逆に見通しが悪くなる。
Jotai を理解するときの見方
Jotai を難しく考えすぎないためには、次のように捉えると分かりやすい。
useState- コンポーネントの中だけで持つ状態
- Jotai の atom
- コンポーネントの外に出して共有できる状態
もちろん実際には派生 atom や非同期 atom など、もう少し広い機能がある。
ただ、最初はこの理解で十分である。
「共有が必要になった useState を、より扱いやすくしたもの」くらいのイメージから入ると、かなり掴みやすい。
まとめ
状態管理とは、難しいライブラリの名前ではなく、変わる値をどこに置き、誰が持ち、どう更新するかを整理すること である。
最初は次の理解で十分だと思う。
- 状態は UI や処理を変える値
- ローカルに閉じるものはローカルに置く
- 複数箇所で使うものだけ共有する
- ライブラリは複雑さを整理するための道具
- Jotai は「共有したい状態を小さく自然に持つ」ための選択肢
状態管理が分からなくなるときは、だいたい「どのライブラリを使うか」から考えてしまっている。
まずは「この値は誰のものか」を考えるところから始めると、かなり理解しやすくなる。
関連記事
Zod とは何かを理解する
Zodとは何か、TypeScriptの型だけでは足りない理由を自分なりに整理する。
「SQLアンチパターン第2版」の感想
『SQLアンチパターン第2版』を読んで印象に残った点と、業務や設計レビューに活かせそうだと感じた内容のメモ。
Astro で個人ブログを始めるときに最初に決めたこと
Astro と Cloudflare Pages を前提に、個人ブログの初期設計で決めたことをまとめる。