Reactはコンポーネントに予測可能性を求めます。同じstateとpropsを与えれば、常に同じUIを描画すべきです。しかし、ほとんどの実際のアプリケーションはその「泡(bubble)」の中に留まることはできません。外部に働きかける必要があるからです。ダッシュボードはサーバーから最新の数値を必要とし、チャットウィジェットはメッセージをリッスンする必要があり、タイマーは刻み続ける必要があります。これらの操作はサイドエフェクト(副作用)であり、Reactのレンダーサイクル(描画サイクル)の外側に存在します。useEffectフックは、コンポーネント自体が誠実であり続けるために、そうした煩雑で予測不可能な処理を配置するための場所です。

サイドエフェクト:useEffect 内に含めるべきもの

サイドエフェクトとは、JSXを返すこと以外の、世界の外部に触れるあらゆる操作のことです。Reactのレンダーフェーズは純粋(pure)であるべきです。データのフェッチを開始したり、グローバル変数に書き込んだり、DOMにリスナーをアタッチしたりするとき、あなたは純粋な領域から一歩踏み出しています。

一般的な例には以下が含まれます:

  • APIからのデータフェッチ
  • タイマーやインターバルの設定
  • windowdocument へのイベントリスナーの追加
  • ブラウザのタブタイトルの更新
  • WebSocketsへの接続

これらのタスクには共通点があります。それは、コンポーネントのreturn文の中や、メインのレンダーロジックの中には属さないということです。setIntervalのようなブラウザAPIをレンダーボディ内で直接呼び出そうとすると、レンダーのたびに実行されてしまい、重複したタイマーが作成され、予期しない挙動を引き起こします。useEffectは、まさにこの作業を分離し、適切なタイミングで実行するために存在します。

依存関係配列(Dependency Array)によるタイミングの制御

useEffectの第2引数は依存関係配列であり、これはクラスコンポーネントから移行してきた開発者にとって最大の混乱の種となります。これは、現在のレンダーの後にエフェクトをスキップするか、あるいは実行するかをReactが判断するために監視する変数のセットだと考えてください。

繰り返し使用することになる3つのパターンがあります。

依存関係配列を全く指定しない。 配列を完全に省略すると、Reactは最初のレンダーを含め、すべてのレンダーの後にエフェクトを実行することを想定します。これは、必要とされるケースは稀です。もしエフェクトがネットワークリクエストや重いDOM操作を行う場合、キー入力やstateの微調整のたびに実行されると、パフォーマンスが著しく低下します。このパターンは、どのpropやstateが変化するかわからないため、何らかの変更があった場合にどうしても再実行する必要がある場合にのみ使用してください。

空の配列 [] これは、コンポーネントがマウントされ、DOMの準備が整った直後に、エフェクトを一度だけ実行するようにReactに指示します。これは初期データのフェッチに最適な場所です。例えば、コンポーネントがユーザープロファイルデータを読み込む場合、ユーザーがページ下部のフォームを操作するたびではなく、プロファイルページが表示されたときに正確に一度だけリクエストを実行したいはずです。

特定の変数を含む配列 [count] これは精密なツールです。Reactは、これらの依存関係の現在の値と、前回のレンダー時の値を比較します。どれか一つでも変更されていれば、エフェクトが実行されます。リスト内の何も変更されていなければ、Reactはエフェクトを完全にスキップします。

ブラウザのタブタイトルをstate変数と同期させている場合、その変数を依存関係配列に入れます。そうすることで、Reactはその値が変化したときにのみタイトルを更新します。配列に入れ忘れると、タイトルは古いままになります。無関係なstate変数を入れてしまうと、重要ではない変更に対してもタイトルを更新するためにサイクルを浪費することになります。

クリーンアップは必須である

エフェクトの中には、痕跡を残すものがあります。タイマーはカウントし続け、イベントリスナーは発火し続け、WebSocketは開いたままになります。コンポーネントがアンマウントされたとき、あるいは依存関係が変更されてエフェクトが再実行されるとき、Reactは前のエフェクトの残骸を自動的にはクリーンアップしません。それはあなたの仕事です。

useEffectの中から関数を返すことで、クリーンアップ関数を作成できます。Reactは、次のエフェクトを適用する前、およびコンポーネントが画面から消えるときに、このクリーンアップ関数を呼び出します。

以下のような場合にはクリーンアップを使用すべきです:

  • clearIntervalclearTimeout によるインターバルやタイムアウトの解除
  • windowdocument、または外部ノードに追加されたイベントリスナーの削除
  • データストリームやサービスからの購読解除(Unsubscribe)

これを怠ると、メモリリークが発生します。コンポーネントがマウントされ、スクロールリスナーをアタッチし、アンマウントしても、リスナーが残ったままになります。ブラウザはコールバックと、それが参照しているDOMノードを保持し続けます。時間の経過とともに、特にナビゲーションが頻繁に行われるシングルページアプリケーション(SPA)では、これらの「幽霊」が蓄積し、タブの動作を重くします。解決策は通常、追加したものを削除する関数を返すという、わずか数行のコードで済みます。

本番環境にリリースされがちなよくある間違い

Even experienced developers reach for useEffect when a simpler option exists. Here are three patterns that should raise a red flag during code review.

Infinite loops. Never update a state variable inside useEffect if that same variable sits in your dependency array, unless you have a gate condition that breaks the cycle. If you read count, increment it, and list count as a dependency, React sees the change, re-renders, runs the effect again, increments again, and locks the browser.

Unnecessary effects. Do not use useEffect to calculate a value from existing props or state. If you can derive it directly during render, just do it. Derived values belong in the component body or in a memoized calculation with useMemo. Moving them into an effect splits your logic across render and effect phases for no benefit and makes the code harder to follow.

The wrong tool for user actions. `use