第5部:高度なフロントエンド Step 14 / 24

状態管理

Reactの状態管理パターンを理解し、適切な方法を選択できるようになりましょう。

状態の種類

ローカル状態

コンポーネント内でのみ使う状態

  • ・フォームの入力値
  • ・モーダルの開閉
  • ・ローディング状態

→ useState

グローバル状態

複数コンポーネントで共有する状態

  • ・ログインユーザー情報
  • ・テーマ設定
  • ・ショッピングカート

→ Context API / Zustand

サーバー状態

APIから取得するデータ

  • ・記事一覧
  • ・ユーザープロフィール
  • ・コメント

→ SWR / React Query

URL状態

URLに含まれる状態

  • ・検索クエリ
  • ・ページ番号
  • ・フィルター条件

→ useSearchParams

useState(ローカル状態)

"use client";

import { useState } from "react";

export function Counter() {
  // 基本的な使い方
  const [count, setCount] = useState(0);

  // オブジェクトの場合
  const [form, setForm] = useState({
    title: "",
    content: "",
  });

  // 配列の場合
  const [items, setItems] = useState<string[]>([]);

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

      {/* オブジェクトの更新 */}
      <input
        value={form.title}
        onChange={(e) => setForm({ ...form, title: e.target.value })}
      />

      {/* 配列への追加 */}
      <button onClick={() => setItems([...items, "new"])}>追加</button>
    </div>
  );
}

Context API(グローバル状態)

contexts/ThemeContext.tsx

"use client";

import { createContext, useContext, useState, ReactNode } from "react";

type Theme = "light" | "dark";

type ThemeContextType = {
  theme: Theme;
  toggleTheme: () => void;
};

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");

  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }
  return context;
}

使用例

"use client";

import { useTheme } from "@/contexts/ThemeContext";

export function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme}>
      現在: {theme} | クリックで切り替え
    </button>
  );
}

カスタムフック

hooks/useLocalStorage.ts

"use client";

import { useState, useEffect } from "react";

export function useLocalStorage<T>(key: string, initialValue: T) {
  // 初期値の設定
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === "undefined") {
      return initialValue;
    }
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  // 値が変更されたらlocalStorageに保存
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue] as const;
}

// 使用例
function App() {
  const [name, setName] = useLocalStorage("name", "");

  return (
    <input value={name} onChange={(e) => setName(e.target.value)} />
  );
}

状態管理の選び方

状況 推奨 理由
1つのコンポーネント内 useState シンプルで十分
親子で共有 props 明示的なデータフロー
深い階層で共有 Context API バケツリレー回避
複雑なグローバル状態 Zustand シンプルなAPI
サーバーからのデータ SWR / React Query キャッシュ・再検証が便利

まとめ

  • ローカル状態はuseState
  • グローバル状態はContext APIまたはZustand
  • サーバー状態はSWR/React Query
  • 必要最小限の状態管理で始める
Next.jsで認証 次へ:フォーム処理