やあ。最近、コードレビューをしていて「あー、またここでハマってるな」って思うポイントがあるんだよね。そう、王様の顔をした諸刃の剣、`useEffect`だ。
中級に差し掛かったエンジニアによくあるのが、「とりあえず画面が表示された後にデータを取ってきたいから、末尾に空の配列 `[]` を置いとけばいいんでしょ?」っていうノリ。
……気持ちは痛いほど分かる。動くには動くからね。でも、その「なんとなく」の理解のままだと、いつか複雑な非同期処理やカスタムフックの設計で痛い目を見る。ブラウザのレンダリングパイプラインとReactの哲学が、裏でどう連携しているのか。そのメカニズムを腹落ちさせておかないと、実務の現場では通用しない。
今日は、そんな`useEffect`の基本構造と、ブラウザの裏側の動きを徹底的に紐解いていこうか。
—
1. useEffectの基本構造:引数と依存配列の正体
まずは基本の「キ」から。`useEffect`は、Reactのレンダリング結果(DOMツリー)がブラウザに描画された後に、外部の世界(API、DOMの直接操作、タイマーなど)と同期するためのフックだ。
構文自体はシンプルだけど、それぞれの役割を正確に言えるかい?
useEffect(() => {
// 1. 副作用関数(Effect Callback)
// ここにDOMの変更後に実行したい処理を書く
return () => {
// 2. クリーンアップ関数(Cleanup Function)
// 次回のエフェクト実行前、またはアンマウント時に実行される
};
}, [/ 3. 依存配列(Dependency Array) /]);
現場で一番誤解されやすいのが、「依存配列は、処理を実行するためのトリガーである」という勘違いだ。
違うんだよ。依存配列の本質は「この変数が変わったら、前回の世界と今の世界にズレが生じるから、エフェクトをもう一回やり直して(同期して)くれ」というReactへの宣言なんだ。
依存配列の3つのパターンとブラウザの挙動
1. 配列なし (`undefined`)
- 挙動: コンポーネントがレンダリングされるたび(毎フレーム)に実行される。
- 現場の判断: 基本的にNG。無限ループの温床になるか、パフォーマンスの猛烈な低下を招く。使うとしたら、極めて特殊なDOM測定のケースくらい。
2. 空の配列 (`[]`)
- 挙動: マウント時(初回レンダリング後)に一度だけ実行される。
- 現場の判断: 「マウント時の初期化処理」の定番。ただし、ここに入れた変数(クロージャ内の値)は固定されるため、stale closure(古い状態のキャプチャ)のバグを生みやすいので注意が必要。
3. 値を入れる (`[propA, stateB]`)
- 挙動: 初回マウント時、および指定した値のいずれかが前回のレンダリング時と異なると判定された場合に実行される。
- 現場の判断: これが最も健全な状態。Reactに「この値とエフェクトを常に同期させろ」と正しく命令できている証拠だ。
—
2. ブラウザの裏側で何が起きているのか?(レンダリングサイクル)
「レンダリング」と「ペイント」を混同していないかい?ここがシニアとジュニアを分ける分水嶺だ。
Reactのライフサイクルにおいて、`useEffect`はブラウザの描画をブロックしない。大まかなタイムラインはこうだ。
1. State / Propsの変更を検知し、Reactがコンポーネント関数を実行する(Render Phase)。
2. Reactが新しい仮想DOMと古い仮想DOMを比較し、実際のDOMへの変更点を計算する。
3. Reactが実際のDOMを更新する。
4. 【重要】ブラウザが画面をピクセル単位で再描画する(Paint)。 ユーザーの目に新しい画面が映る。
5. 描画が完了した「後」に、非同期で `useEffect` のコールバックが実行される。
なぜReactは、DOM更新の「直後」ではなく「描画の後」に`useEffect`を走らせるのか?
答えは簡単で、ユーザーの体感パフォーマンス(UX)を絶対に落とさないためだ。もしDOM更新の直後、描画の前に重いデータフェッチや計算処理を挟んだら、画面の表示がカクつく(フレームレートが落ちる)だろ?
Reactは「とにかく先にユーザーに画面を見せろ、副作用はそのあとだ」という優しい設計になっているのさ。
—
3. 実務で即戦力になるサンプルコード
理屈はこれくらいにして、現場でそのまま使える、綺麗に整理されたサンプルコードを見てみよう。
ウィンドウの幅(リサイズ)を監視して、画面に表示しつつ、コンポーネントが消えるときにイベントリスナーを綺麗に片付ける(クリーンアップする)例だ。
import React, { useState, useEffect } from ‘react’;
export const WindowWidthChecker = () => {
// ウィンドウの幅を保持するステート
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
// 1. 副作用内で実行するハンド関数を定義
const handleResize = () => {
// 現在のウィンドウ幅をstateに反映
setWindowWidth(window.innerWidth);
};
// 2. ブラウザのresizeイベントにリスナーを登録
window.addEventListener(‘resize’, handleResize);
// デバッグ用(実務では消すこと)
console.log(‘【useEffect】イベントリスナーを登録しました’);
// 3. クリーンアップ関数の返却
// コンポーネントがアンマウントされる時、または再実行される直前に呼ばれる
return () => {
window.removeEventListener(‘resize’, handleResize);
console.log(‘【Cleanup】古いイベントリスナーを削除しました’);
};
}, []); // 依存配列が空なので、マウント時の一度だけ登録・解除が行われる
return (
ウィンドウ幅監視コンポーネント
現在のウィンドウ幅: {windowWidth}px
※F12でコンソールを開きながらブラウザの幅を変えて、挙動を確認してみてください。
);
};
このコードの優れたポイント(シニアからの解説)
- メモリリークの防止: `window.addEventListener` を登録したら、必ずセットで `removeEventListener` をクリーンアップで行っている。これをサボると、コンポーネントが消えてもリスナーが残り続け、メモリリークや思わすバグの温床になる。
- 依存配列の正確性: ウィンドウのリサイズ関数はマウント時に1度登録すれば、ReactのState更新機能(`setWindowWidth`)が最新の値を安全にクロージャ越しに扱ってくれるため、依存配列を `[]` にしても安全に機能する。
—
シニアアーキテクトからのメッセージ
`useEffect`は、Reactの世界と「外部の世界」を繋ぐ唯一無二のブリッジだ。
だからこそ、適当に扱えばスパゲッティコードの元凶になるし、正しく扱えばこれほどエレガントに副作用をカプセル化できる仕組みもない。
「なぜこのタイミングで走るのか」「この変数は本当に依存配列に入れるべきか」「クリーンアップは必要か」――この3つを常に自問自答する癖をつけてほしい。
君の書くコードの品質は、こうした細部へのこだわりから確実に研ぎ澄まされていくはずだ。さあ、次のタスクに取り掛かろうか!

コメント