第3回

useEffectの依存配列:正しい指定方法とよくある落とし穴

useEffectの依存配列を徹底解説。空配列・値の指定・オブジェクトや関数を依存配列に含めたときの問題と解決策、exhaustive-depsルールの使い方を実例付きで説明します。

·10分で読める

依存配列の役割

依存配列は useEffect に「いつ実行するか」を伝えます。

useEffect(() => {
  // この処理を...
}, [a, b]);  // ... a か b が変わったときに実行する

Reactは前回のレンダリングと今回のレンダリングで依存配列の値を比較します。変わっていれば副作用を再実行し、変わっていなければスキップします。

3パターンの使い分け

// パターン1:毎回(依存配列なし)
useEffect(() => {
  console.log('毎回のレンダリング後に実行');
});

// パターン2:マウント時に1回だけ(空配列)
useEffect(() => {
  console.log('マウント時だけ実行');
}, []);

// パターン3:特定の値が変わったとき
useEffect(() => {
  console.log(`userId が ${userId} に変わった`);
}, [userId]);

実務ではパターン2と3が主です。パターン1は慎重に使います(後述)。

依存配列に含めるべき値

useEffect内で参照しているすべての外部の値を依存配列に含めます。

function SearchResults({ query, pageSize }: { query: string; pageSize: number }) {
  const [results, setResults] = useState<string[]>([]);

  useEffect(() => {
    // query と pageSize を使っている → 両方を依存配列に含める
    fetch(`/api/search?q=${query}&size=${pageSize}`)
      .then((res) => res.json())
      .then(setResults);
  }, [query, pageSize]);  // ← 両方必要

  return <ul>{results.map((r, i) => <li key={i}>{r}</li>)}</ul>;
}

オブジェクト・配列を依存配列に含めると無限ループになる

Reactは依存配列の値を Object.is(参照の等値比較)で比較します。オブジェクトや配列はレンダリングのたびに新しいオブジェクトとして作られるため、「変わった」と判定され続けます。

function BadExample({ config }: { config: { limit: number } }) {
  const [data, setData] = useState([]);

  // ❌ config はレンダリングのたびに新しいオブジェクトが作られる
  // → 毎回「変わった」と判定 → 無限ループ
  useEffect(() => {
    fetch(`/api/data?limit=${config.limit}`)
      .then((res) => res.json())
      .then(setData);
  }, [config]);
}

解決策1:プリミティブな値を使う

// ✅ オブジェクトの中のプリミティブな値を使う
useEffect(() => {
  fetch(`/api/data?limit=${config.limit}`)
    .then((res) => res.json())
    .then(setData);
}, [config.limit]);  // プリミティブな値なら安全

解決策2:useMemoで安定させる

const stableConfig = useMemo(() => ({ limit }), [limit]);

useEffect(() => {
  fetch(`/api/data?limit=${stableConfig.limit}`)
    .then((res) => res.json())
    .then(setData);
}, [stableConfig]);

関数を依存配列に含めると無限ループになる

同様に、関数もレンダリングのたびに新しい関数が作られます。

function BadExample({ onLoad }: { onLoad: (data: unknown) => void }) {
  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.json())
      .then(onLoad);
  }, [onLoad]);  // ❌ onLoad が毎回新しい関数 → 無限ループ
}

解決策:useCallbackで関数を安定させる

function Parent() {
  const handleLoad = useCallback((data: unknown) => {
    console.log('読み込んだ:', data);
  }, []);  // 依存なし → 常に同じ関数

  return <Child onLoad={handleLoad} />;
}

依存配列のよくある間違いと修正

間違い1:使っている値を依存配列から省く

// ❌ userId を使っているのに依存配列が空
useEffect(() => {
  fetchUser(userId).then(setUser);
}, []);  // userId の変化を無視してしまう

// ✅ 正しく依存配列に含める
useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId]);

間違い2:不要な値を依存配列に入れる

// ❌ setUser はstateのsetter。Reactが安定性を保証するので依存配列不要
useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId, setUser]);  // setUser は不要

// ✅
useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId]);

Reactの useStateuseReducer の更新関数は参照が安定しているため、依存配列に含める必要はありません。

ESLintのexhaustive-depsルール

eslint-plugin-react-hooksexhaustive-deps ルールを有効にすると、依存配列の漏れを自動検出できます。

{
  "rules": {
    "react-hooks/exhaustive-deps": "warn"
  }
}

エディタが依存配列の誤りを指摘してくれるため、積極的に活用しましょう。

たける
たける 依存配列、毎回何を入れればいいかわかって入れてるつもりが、実行してみると無限ループになったり逆に動かなかったりで混乱します。
りこ
りこ ESLintの `exhaustive-deps` ルールを有効にして、警告を全部直す癖をつけて。手で考えるより確実。
たける
たける でも警告を消すために依存配列に追加したら無限ループになって、`// eslint-disable` で黙らせてしまいました……。
りこ
りこ それは根本を直さないといけない。無限ループになるのはオブジェクト・配列・関数が依存配列に入っているのが原因がほとんど。`eslint-disable` で黙らせるのは最後の手段。

まとめ

  • 依存配列にはuseEffect内で使うすべての外部の値を含める
  • オブジェクト・配列・関数は毎回新しく作られるため、そのまま依存配列に入れると無限ループになる
  • 解決策は「プリミティブな値に分解する」か「useMemo/useCallback で安定させる」
  • ESLintの exhaustive-deps ルールで依存配列の漏れを自動検出する

次の第4回では、タイマーやイベントリスナーのクリーンアップを中心に useEffectのクリーンアップ を学びます。