Reactに触れる中で「状態管理」という言葉に出会う。
なんとなく使ってしまっていて、何が状態で、何を管理していて、なぜライブラリが必要になるのかが分かりにくい。

https://www.youtube.com/watch?v=PZY1x4OqQks&t=700s の動画を見て、あらためて学びなおそうと思い、この記事を書くことにした。

まずは、管理する「状態」とは何かに注目して整理してみる。

状態とは何か

状態とは、“その瞬間の画面やアプリのふるまいを決める値”のことである。

たとえば次のようなものである。

  • モーダルが開いているかどうか
  • 入力フォームに今入っている文字
  • ログイン中のユーザー情報
  • 商品一覧の取得結果
  • 現在選択中のタブ
  • カートに入っている商品

逆に、毎回固定で変わらない値は状態ではない。

  • サイト名
  • 固定メニュー
  • 定数の設定値

なぜ状態を管理する必要があるのか

画面というのは、結局のところ状態を見て描画されている。
だから状態が増えるほど、「どこで値を持つか」「誰が更新するか」「どこまで共有するか」を考えないと破綻しやすくなる。

簡単な例で考える。

const [isOpen, setIsOpen] = useState(false);

これは「モーダルが開いているかどうか」を持つ状態である。
このくらいなら単純で、そのコンポーネントの中だけで完結する。

しかしアプリが大きくなると、次のようなことが起こる。

  • ヘッダーのボタンでもモーダルを開きたい
  • 別コンポーネントからも同じ値を見たい
  • API の取得結果を複数画面で共有したい
  • 入力内容をページ移動後も保持したい

こうなると、ただ useState を置くだけでは整理しにくくなる。
この「増えてきた状態を破綻しないよう扱うこと」が、状態管理の本質である。

ローカルな状態とグローバルな状態

状態管理を考えるとき、最初に区別したいのがローカルな状態とグローバルな状態である。

ローカルな状態

そのコンポーネントの中だけで閉じている状態。

  • 開閉フラグ
  • 一時的な入力値
  • hover 状態
  • UI 上のちょっとした切り替え

これは useStateuseReducer で十分なことが多い。

グローバルな状態

複数のコンポーネントや画面で共有したい状態。

  • ログインユーザー
  • テーマ設定
  • カートの中身
  • フィルタ条件
  • どこからでも参照したい共通 UI 状態

こちらは、props の受け渡しだけではつらくなりやすい。
そのため、Context や専用ライブラリが登場する。

props drilling がつらくなる瞬間

状態管理ライブラリが欲しくなる理由として、よく出てくるのが props drilling である。

これは、必要な値を深い子コンポーネントまで渡すために、途中のコンポーネントが使わない props を延々と中継する状態を指す。

例えばこういう状況である。

  1. 親がユーザー情報を持っている
  2. 孫コンポーネントでそれを使いたい
  3. 間の子コンポーネントは使わないのに 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 は「共有したい状態を小さく自然に持つ」ための選択肢

状態管理が分からなくなるときは、だいたい「どのライブラリを使うか」から考えてしまっている。
まずは「この値は誰のものか」を考えるところから始めると、かなり理解しやすくなる。

関連記事