⚛️ React Boundary
内部理解 — Hooks

useEffectは何のためにあるのか?

「とりあえずuseEffectに書く」から脱却しよう。副作用の本質、レンダリングとの分離、そして2回実行の謎を解説します。

🎯 結論から言う

useEffectは「Reactの外の世界」と同期するための仕組みです。

Reactはコンポーネント関数を純粋関数として扱いたい——同じ入力(props/state)に対して常に同じ出力(Virtual DOM)を返すべきだという設計思想があります。 しかし現実のアプリには「副作用」が必要です。useEffectはその副作用をレンダリングから分離して安全に実行するための仕組みです。

🧩 「副作用」とは何か?

副作用(side effect)とは、コンポーネントのレンダリング結果に直接関係しない、外部への影響のことです。

🌐

API・データフェッチ

サーバーへのHTTPリクエスト。レンダリングとは別のタイミングで行う必要がある。

📡

イベントリスナー

windowへのaddEventListener。コンポーネントが消えたら必ず解除する必要がある。

⏱️

タイマー

setInterval / setTimeout。コンポーネントのライフサイクルに合わせて管理が必要。

📦

外部ライブラリの初期化

DOM直接操作ライブラリ(地図・チャートなど)の初期化。DOMが存在してから実行が必要。

⚠️ なぜレンダリング中に副作用を書いてはいけないのか?

コンポーネント関数のトップレベルで副作用を実行すると、何が起こるでしょうか?

// ❌ 悪い例: レンダリング中に副作用
function UserProfile({ userId }) {
  // レンダリングのたびにAPIを呼ぶ!
  fetch('/api/user/' + userId);
  
  return <div>...</div>;
}

問題点

  • Reactは再レンダリングのたびにコンポーネント関数を呼ぶ——fetchが何度も走る
  • React 18のConcurrent Modeでは、レンダリングが中断・再実行されることがある
  • 副作用をレンダリング中に混ぜると、Reactが最適化できなくなる

✅ useEffectの正しい使い方

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // ✅ レンダリング後に副作用を実行
    fetch('/api/user/' + userId)
      .then(r => r.json())
      .then(data => setUser(data));

    // クリーンアップ関数(オプション)
    return () => {
      // コンポーネント消去時 or 次のEffect実行前に呼ばれる
    };
  }, [userId]); // 依存配列: userIdが変わったら再実行

  return <div>{user?.name}</div>;
}

依存配列(dependency array)の意味

[] マウント時に1回だけ実行
[a, b] a または b が変化したときに再実行
なし 毎回のレンダリング後に実行(ほぼ使わない)

🔍 なぜStrictModeで2回実行されるのか?

開発環境でuseEffectが2回実行される——これはReact 18のStrictModeが意図的に行っていることです。

StrictModeが2回実行する理由

Reactは「コンポーネントはクリーンアップして再マウントしても壊れないか?」を検証します。 将来的にReactは状態を保持したままコンポーネントをアンマウント・再マウントする最適化(Fast Refresh、Offscreen API等)を行う予定があり、 その耐性を開発時にチェックするために意図的に2回実行します。

本番環境では1回だけ実行されます。

useEffect(() => {
  const subscription = externalStore.subscribe(callback);
  
  // ✅ クリーンアップを返す
  // StrictModeでは: setup → cleanup → setup の順で実行
  return () => {
    subscription.unsubscribe();
  };
}, []);

💡 useEffectを使わなくていい場合

「useEffectに何でも書く」は間違いです。以下のケースはuseEffectが不要です。

{[ { bad: 'useEffect内でstateからstateを計算', good: 'レンダリング中に直接計算(useMemo)' }, { bad: 'propsが変わったときstateをリセット', good: 'keyを変えてコンポーネントを再マウント' }, { bad: 'イベントハンドラのロジック', good: 'イベントハンドラ関数に直接書く' }, ].map(item => (
{item.bad}
{item.good}
))}

📌 まとめ

  • ✓ useEffectは「Reactの外の世界(DOM・API・サブスクリプション)」と同期するための仕組み
  • ✓ レンダリング中に副作用を書くと、Reactの最適化を壊す
  • ✓ クリーンアップ関数を返すことで、メモリリークを防ぐ
  • ✓ StrictModeの2回実行は意図的——クリーンアップが正しく書けているかのテスト
  • ✓ stateからstateを計算するだけならuseEffectは不要

関連記事

}