はじめに
Reactコンポーネントで副作用(データ取得・購読・手動DOM操作等)を扱う際の要がuseEffect
フックです。しかし「いつ呼ばれるのか」「どこでクリーンアップすべきか」「依存配列の指定をどう書くべきか」を誤解しやすく、バグや無限ループ、不要な再実行に悩む開発者は少なくありません。本稿ではuseEffect
とuseLayoutEffect
のライフサイクルを丁寧に紐解き、実践的なコード例とともに解説します。
Reactエフェクトの基本概念
エフェクト(副作用)とは、ReactのUI更新の外側で起きる処理全般を指します。具体例としては:
- 外部APIからデータを取得する
- WebSocketやイベントリスナーを登録・解除する
- 手動でDOMを操作する(サードパーティライブラリ連携)
useEffect
フックは、レンダー後に“副作用”を実行し、必要ならクリーンアップ(解除)も指定できる仕組みです。
import { useEffect } from 'react';
function Example({ url }) {
useEffect(() => {
fetch(url).then(r => r.json()).then(data => {
console.log(data);
});
}, [url]);
return <div>データを取得中…</div>;
}
- 第1引数:エフェクト用コールバック
- 第2引数:依存配列。中身が変わったときだけ再実行
ライフサイクルフロー全体像
- マウント後:最初のレンダーが完了した直後に実行
- 更新後:依存配列の値が前回と異なるとレンダー後に再実行
- アンマウント前:前回のエフェクトで返したクリーンアップ関数を実行
- 依存配列なし:マウント後のみ実行、更新時には再実行されない
- 依存配列を省略:レンダーごとに実行し、旧エフェクトは都度クリーンアップ
以下、各ステップを深掘りします。
マウント後の初回実行
useEffect(fn, deps)
は マウント直後 にまず実行されます。DOMが構築された後なので、document.getElementById
など直接DOM操作しても正しく参照できるタイミングです。
function InitLogger() {
useEffect(() => {
console.log('コンポーネントが画面に追加されました');
}, []); // 依存配列空でマウント後のみ
return <div>初期化ログを出力</div>;
}
- 依存配列を空にすると、初回レンダー後だけに限定
- 一度きりのサブスクリプションやログに最適
依存配列による再実行制御
依存配列 ([a, b, …]
) に指定した値が前回と異なる場合、更新後に再実行されます。
function Fetcher({ userId }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
let active = true;
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
if (active) setProfile(data);
});
return () => { active = false }; // クリーンアップ
}, [userId]);
// userIdが変わるたびに実行/前回のフェッチはキャンセル
return profile ? <UserCard data={profile} /> : <p>読み込み中…</p>;
}
- 古いフェッチ結果を書き込まないためのフラグ制御
- 依存の追加漏れが無限ループや stale closure を招く
クリーンアップ関数の重要性
エフェクト内で何らかの購読(Subscription)やタイマーを登録した場合、コンポーネントの破棄前に必ず解除する必要があります。useEffect
のコールバックが返す関数がクリーンアップに該当します。
function Clock() {
const [time, setTime] = useState(Date.now());
useEffect(() => {
const id = setInterval(() => setTime(Date.now()), 1000);
return () => clearInterval(id); // アンマウント時に解除
}, []);
return <div>現在時刻:{new Date(time).toLocaleTimeString()}</div>;
}
- クリーンアップがないと、メモリリークや不要なタイマーが残り続ける
- レンダーごとに再登録してしまう実装ミスにも注意
useLayoutEffectとuseEffectの違い
useEffect
:レンダー後に非同期的に実行されるuseLayoutEffect
:レンダー直後、ブラウザの描画前に同期的に実行される
レイアウトやサイズ計算をDOMに対して行う場合は、レイアウトズレを防ぐために useLayoutEffect
を使います。それ以外は通常の useEffect
を優先しましょう。
import { useLayoutEffect, useRef } from 'react';
function MeasureBox() {
const ref = useRef();
useLayoutEffect(() => {
console.log(ref.current.getBoundingClientRect());
}, []);
return <div ref={ref}>サイズを測定</div>;
}
副次的レンダーとスケジューリング
React 18では厳格モード(Strict Mode)使用時に、開発中の副作用が2回実行される仕様があります。これは初回マウント→アンマウント→再マウントの動きを通して、クリーンアップ漏れを早期に検出するためです。本番ビルドでは1回だけ実行されますが、開発体験向上のため注意して実装しましょう。
パフォーマンス最適化パターン
- 依存配列の最小化
不要な変数を入れると再実行が多発。逆に入れ忘れると stale data の原因。 useCallback
/useMemo
エフェクト中で関数や計算結果を依存させる場合はメモ化して参照の安定化を図る。- サブスクリプション分離
複数の副作用を一つのuseEffect
にまとめず、責務ごとに分けると、不要な再実行を抑えやすい。
useEffect(() => { /* データ取得 */ }, [id]);
useEffect(() => { /* 購読登録 */ }, [socket]);
よくある落とし穴と対策
症状 | 原因 | 対策 |
---|---|---|
無限ループ | 依存配列に頻繁に変わる値が含まれる | 必要最小限の依存に絞る |
クリーンアップ漏れ | return を忘れた | 購読/タイマーは必ず return で解除 |
stale closure(古い状態を参照) | クロージャ内で古い変数を直接参照 | 依存配列に必要な値をすべて含める、またはフラグ制御 |
レンダリング前の計測ずれ | useEffectでDOM計測 | useLayoutEffectに移行 |
実践例:データフェッチ×キャンセルパターン
import { useState, useEffect } from 'react';
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort(); // クエリ変更時・アンマウント時に中断
}, [query]);
return (
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
);
}
まとめ
Reactの副作用ライフサイクルを正しく理解すると、UI更新と非同期処理、リソース管理がスムーズになります。useEffect
とuseLayoutEffect
の使い分け、依存配列の設計、クリーンアップの徹底、パフォーマンス最適化パターンをマスターし、副作用バグから解放された堅牢なコンポーネントを開発しましょう。