<CodeLearn/>
状態管理 レッスン1

状態管理の基本

なぜ状態管理が必要なのか?問題と解決策を理解しよう

なぜ状態管理が必要か?

小さなReactアプリでは、useStateだけで十分に状態を管理できます。 しかし、アプリが大きくなると次のような問題が発生します。

  • 複数のコンポーネントで同じデータを共有したい
  • 状態の更新ロジックが複雑になる
  • データの流れが追いにくくなる
  • コンポーネントの再レンダリングが増えてパフォーマンスが低下する
// 小さなアプリ:useStateで十分
function SimpleApp() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

// 大きなアプリ:状態の共有が複雑になる
// ユーザー情報、テーマ、通知、カートの中身...
// これらを複数のページ・コンポーネントで共有するには?

Props Drilling問題

Props Drillingとは、深くネストされたコンポーネントにデータを渡すために、 中間のコンポーネントが不要なpropsをバケツリレーのように受け渡す問題です。

// Props Drilling の悪い例
// App → Layout → Sidebar → UserMenu → Avatar
// userデータが4階層も受け渡される

function App() {
  const [user, setUser] = useState({ name: "太郎", avatar: "/img.png" });
  return <Layout user={user} />;        // 1階層目
}

function Layout({ user }) {
  return (
    <div>
      <Sidebar user={user} />           {/* 2階層目(Layoutはuserを使わない) */}
      <main>コンテンツ</main>
    </div>
  );
}

function Sidebar({ user }) {
  return <UserMenu user={user} />;      {/* 3階層目(Sidebarもuserを使わない) */}
}

function UserMenu({ user }) {
  return <Avatar src={user.avatar} />;  {/* 4階層目:やっと使う */}
}

function Avatar({ src }) {
  return <img src={src} alt="avatar" />;
}

LayoutやSidebarはuserを使わないのに、 下の階層に渡すためだけにpropsを受け取っています。これがProps Drillingです。

Lifting State Up(状態の引き上げ)

複数のコンポーネントで状態を共有する最も基本的な方法がLifting State Up(状態の引き上げ)です。 共通の親コンポーネントに状態を配置し、子コンポーネントにpropsとして渡します。

// 状態の引き上げの例
// 2つの入力フィールドの値を同期させる

function TemperatureConverter() {
  // 共通の親で状態を管理
  const [celsius, setCelsius] = useState(0);

  const fahrenheit = celsius * 9 / 5 + 32;

  return (
    <div>
      <TemperatureInput
        label="摂氏(°C)"
        value={celsius}
        onChange={(val) => setCelsius(val)}
      />
      <TemperatureInput
        label="華氏(°F)"
        value={fahrenheit}
        onChange={(val) => setCelsius((val - 32) * 5 / 9)}
      />
    </div>
  );
}

function TemperatureInput({ label, value, onChange }) {
  return (
    <label>
      {label}:
      <input
        type="number"
        value={value}
        onChange={(e) => onChange(Number(e.target.value))}
      />
    </label>
  );
}

状態の引き上げはシンプルですが、コンポーネント階層が深くなるとProps Drillingにつながります。

グローバル状態が必要なとき

すべての状態をグローバルにする必要はありません。 以下の基準でローカル状態グローバル状態を使い分けましょう。

// ローカル状態(useState)で十分なケース
// - フォームの入力値
// - モーダルの開閉状態
// - ドロップダウンの選択値
// - アコーディオンの展開状態

function SearchBar() {
  const [query, setQuery] = useState(""); // ローカルで十分
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

// グローバル状態が必要なケース
// - ログインユーザー情報(多くのコンポーネントで参照)
// - テーマ設定(ダーク/ライトモード)
// - 言語設定(i18n)
// - ショッピングカートの中身
// - 通知の一覧

// 判断基準:
// 1. 複数の離れたコンポーネントが同じデータを必要とする
// 2. ページ遷移しても状態を保持したい
// 3. Props Drillingが3階層以上になる

状態管理ソリューションの全体像

Reactの状態管理には多くの選択肢があります。それぞれの特徴を把握しましょう。

// 1. React組み込み
//    useState    → コンポーネントローカルの状態
//    useReducer  → 複雑なローカル状態
//    useContext  → 軽量なグローバル状態

// 2. 外部ライブラリ(中〜大規模向け)
//    Redux Toolkit → 最も採用実績が多い、強力なDevTools
//    Zustand       → 軽量でシンプル、ボイラープレート少ない
//    Jotai         → アトミックな状態管理、React的な設計
//    Recoil        → Meta製、実験的(開発停滞中)

// 3. サーバー状態管理
//    TanStack Query(React Query) → API通信の状態管理
//    SWR                          → Vercel製、シンプル
//    RTK Query                    → Redux Toolkitの一部

// 選び方の目安:
// 小規模アプリ       → useState + useContext
// 中規模アプリ       → Zustand or Jotai
// 大規模・チーム開発  → Redux Toolkit
// API通信がメイン    → TanStack Query + 軽量な状態管理

まとめ

  • アプリが大きくなるとuseStateだけでは管理が難しくなる
  • Props Drillingは中間コンポーネントが不要なpropsを受け渡す問題
  • Lifting State Upは状態共有の基本だが、深い階層では限界がある
  • グローバル状態は「複数の離れたコンポーネントが同じデータを必要とする」場合に使う
  • useContext、Redux Toolkit、Zustand、Jotaiなど目的に応じた選択肢がある