第12回

Hooksのルールとよくある間違い:シリーズ総まとめ

ReactのHooksが守るべきルールを解説します。条件分岐・ループ内での呼び出し禁止の理由、ESLint設定、無限ループや古い値参照などよくある間違いとその対処法をシリーズの集大成としてまとめます。

·12分で読める

Hooksの2つのルール

ReactにはHooksを使う上で必ず守らなければならない2つのルールがあります。

ルール1:トップレベルでのみ呼ぶ

Hookはコンポーネントのトップレベルで呼び出す必要があります。条件分岐・ループ・ネストした関数の中では呼べません。

// ❌ 条件分岐の中でHookを呼ぶ
function Bad({ condition }: { condition: boolean }) {
  if (condition) {
    const [state, setState] = useState(0);  // NG
  }
  // ...
}

// ❌ ループの中でHookを呼ぶ
function BadList({ items }: { items: string[] }) {
  return items.map((item) => {
    const [checked, setChecked] = useState(false);  // NG
    return <li>{item}</li>;
  });
}

// ✅ トップレベルで呼ぶ
function Good({ condition }: { condition: boolean }) {
  const [state, setState] = useState(0);  // OK
  // 条件に基づく処理は中で行う
  if (!condition) return null;
  return <div>{state}</div>;
}

理由:ReactはHooksが毎回同じ順序で呼ばれることを前提に、複数の useState などを管理しています。条件分岐で呼ぶ順序が変わると、Reactが正しく対応するHookの状態を識別できなくなります。

ルール2:ReactコンポーネントまたはカスタムHookの中でのみ呼ぶ

// ❌ 通常の関数の中でHookを呼ぶ
function notAComponent() {
  const [state, setState] = useState(0);  // NG
}

// ✅ Reactコンポーネントの中
function MyComponent() {
  const [state, setState] = useState(0);  // OK
  return <div>{state}</div>;
}

// ✅ カスタムHookの中(useで始まる関数)
function useMyHook() {
  const [state, setState] = useState(0);  // OK
  return state;
}

ESLintで自動検出する

eslint-plugin-react-hooks をインストールすると、ルール違反を自動で検出できます。

npm install --save-dev eslint-plugin-react-hooks
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",    // Hooksのルール違反を検出
    "react-hooks/exhaustive-deps": "warn"      // 依存配列の漏れを検出
  }
}

よくある間違いと対処法

間違い1:useEffectの無限ループ

// ❌ オブジェクトを依存配列に入れると毎回新しい参照になって無限ループ
function Bad() {
  const [data, setData] = useState([]);
  const options = { limit: 10 };  // 毎回新しいオブジェクト

  useEffect(() => {
    fetchData(options).then(setData);
  }, [options]);  // 毎回変わる → 無限ループ
}

// ✅ プリミティブな値を依存配列に入れる
function Good() {
  const [data, setData] = useState([]);
  const limit = 10;

  useEffect(() => {
    fetchData({ limit }).then(setData);
  }, [limit]);
}

間違い2:古いstateを参照する(stale closure)

// ❌ setIntervalのコールバックはクロージャで古いcountを参照し続ける
function Bad() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);  // count は常に0(古い値)
    }, 1000);
    return () => clearInterval(id);
  }, []);  // count を依存配列から除外しているため問題が起きる
}

// ✅ 更新関数(関数形式)を使う
function Good() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount((prev) => prev + 1);  // 常に最新のprevを参照
    }, 1000);
    return () => clearInterval(id);
  }, []);  // 依存なし(更新関数は最新stateを受け取るため)
}

間違い3:useStateの更新が非同期

// ❌ setStateは非同期。直後に最新値を読もうとしてもまだ更新されていない
function Bad() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count);  // まだ古い値
    setCount(count + 1);  // 2回呼んでも1しか増えない
  };
}

// ✅ 更新関数を使って連続更新
function Good() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);  // 2増える
  };
}

間違い4:useRefとuseStateの混同

// ❌ UIに表示する値をuseRefで管理するとUIが更新されない
function Bad() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current += 1;  // 更新しても再レンダリングが起きない
  };

  return <div>{countRef.current}</div>;  // 表示が変わらない
}

// ✅ UIに表示する値はuseState
function Good() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
    </div>
  );
}

間違い5:カスタムHookの命名

// ❌ useで始まらないとESLintが警告し、Reactもhookとして認識しない
function fetchUserData(userId: number) {
  const [user, setUser] = useState(null);
  // ...
}

// ✅ useで始める
function useFetchUser(userId: number) {
  const [user, setUser] = useState(null);
  // ...
  return user;
}
たける
たける Hooksのルールってなんで守らないといけないんですか? 「条件の中でuseStateを呼ぶな」って言われますけど、どう壊れるのか実感がなくて。
りこ
りこ ReactはHookを「呼ばれた順番」で管理している。条件によって呼ぶ回数が変わると、前回との対応がズレてバグになる。壊れ方が実行時エラーじゃなく「違う値が返ってくる」という静かなバグになることもある。
ユナ
ユナ ESLintの `react-hooks` プラグインを入れれば、ルール違反はコードを書いた時点でエラーになる。「なぜ壊れるか」を理解した上で、ツールに検出させるのが実務のやり方。

Hooks シリーズ総まとめ

このシリーズで学んだHookを整理します。

Hook 用途
useState シンプルな状態管理
useEffect 副作用(データ取得・タイマー・DOM操作)
useContext props drillingを避けたグローバル値の共有
useReducer 複雑な状態・複数の連動する更新
useRef DOM参照・レンダリングを起こさない値の保持
useMemo 計算結果のメモ化・参照の安定化
useCallback 関数のメモ化(React.memoとセット)
useTransition 重い更新を非緊急にしてUI応答性を維持
useDeferredValue 外部から受け取った値の遅延

これらのHookを組み合わせたカスタムHookを作ることで、ロジックの再利用性と可読性が上がります。

Hooksのルール(トップレベルで呼ぶ・コンポーネント/カスタムHook内でのみ使う)を守り、ESLintの react-hooks プラグインと組み合わせることで、安全で保守しやすいコードが書けます。