ReactのuseEffect入門:基本から気をつけることまで
はじめに
useEffect はReactコンポーネントで副作用(APIフェッチ・タイマー・イベントリスナーなど)を扱うためのフックです。便利な一方で、使い方を誤ると無限ループやメモリリークが起きやすいため、注意点を理解しておくことが重要です。
useEffectの基本
useEffect(処理, [依存配列]) の形で書きます。
import { useEffect } from 'react'; function Component() { useEffect(() => { console.log('実行された'); }, []); return <div>Hello</div>; }
依存配列で実行タイミングを制御する
第2引数の依存配列によって、いつ処理が走るかが変わります。
空配列 []:マウント時に1回だけ
useEffect(() => { console.log('マウント時に1回だけ実行'); }, []);
初期データの取得など、コンポーネントが表示されたときに1回だけ実行したい処理に使います。
値を指定:その値が変わるたびに実行
const [userId, setUserId] = useState(1); useEffect(() => { console.log(`userId が変わった: ${userId}`); }, [userId]); // userId が変わるたびに実行
依存配列を省略:毎レンダリング後に実行
useEffect(() => { console.log('毎レンダリング後に実行'); });
基本的に使う場面はほとんどありません。意図しない実行が起きやすいので避けた方が無難です。
クリーンアップ:アンマウント時に後片付けする
useEffect の中で return にクリーンアップ関数を書くと、コンポーネントがアンマウントされるとき(または次の useEffect が実行される前)に呼ばれます。
タイマーのクリーンアップ
useEffect(() => { const timer = setInterval(() => { console.log('tick'); }, 1000); return () => { clearInterval(timer); // コンポーネントが消えたらタイマーを止める }; }, []);
クリーンアップを忘れると、コンポーネントが消えてもタイマーが動き続けてメモリリークになります。
イベントリスナーのクリーンアップ
useEffect(() => { const handleResize = () => { console.log(window.innerWidth); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); // リスナーを解除する }; }, []);
クリーンアップが必要なものの目安
setInterval/setTimeoutaddEventListener- WebSocketの接続
- 外部ライブラリのサブスクリプション
非同期処理の書き方
useEffect のコールバックに直接 async はつけられません。
// NG:直接 async はつけられない useEffect(async () => { const data = await fetchUser(); // クリーンアップが返せなくなる }, []);
内部に async 関数を定義してすぐ呼び出す方法が一般的です。
// OK useEffect(() => { const fetchData = async () => { const data = await fetchUser(); setUser(data); }; fetchData(); }, []);
気をつけること
① 依存配列の漏れ
依存配列に必要な値を入れ忘れると、古い値を参照したままになります。
const [count, setCount] = useState(0); // NG:count を使っているのに依存配列に入れていない useEffect(() => { document.title = `カウント: ${count}`; // 常に初期値の 0 を参照する }, []); // OK useEffect(() => { document.title = `カウント: ${count}`; }, [count]);
ESLintの eslint-plugin-react-hooks を使うと依存配列の漏れを自動で警告してくれます。
② オブジェクト・配列を依存配列に入れると無限ループになる
オブジェクトや配列はレンダリングのたびに新しい参照が作られます。依存配列に入れると毎回「変わった」と判断されて無限ループになります。
// NG:レンダリングのたびに新しいオブジェクトが作られて無限ループ function Component() { const options = { limit: 10 }; // 毎レンダリングで新しい参照 useEffect(() => { fetchData(options); }, [options]); // 毎回変わったと判断されて無限ループ }
解決策1:依存配列に入れるのはプリミティブ値(文字列・数値・真偽値)にする
useEffect(() => { fetchData({ limit: 10 }); }, []); // options をコンポーネント外に出すか、依存配列を使わない
解決策2:useMemo や useCallback で参照を固定する
import { useMemo } from 'react'; const options = useMemo(() => ({ limit: 10 }), []); useEffect(() => { fetchData(options); }, [options]); // 参照が変わらないのでループしない
③ 関数を依存配列に入れると無限ループになる
関数も同様に、レンダリングのたびに新しい参照になります。
// NG:毎レンダリングで新しい関数が作られて無限ループ function Component() { const fetchData = () => { /* ... */ }; useEffect(() => { fetchData(); }, [fetchData]); // 無限ループ }
解決策:useCallback で参照を固定する
import { useCallback } from 'react'; const fetchData = useCallback(() => { /* ... */ }, []); // 依存関係が変わらない限り同じ関数参照を返す useEffect(() => { fetchData(); }, [fetchData]); // 参照が固定されるのでループしない
④ アンマウント後に state を更新しようとするとエラーになる
非同期処理中にコンポーネントがアンマウントされると、完了後に存在しないコンポーネントの state を更新しようとしてエラーになります。
useEffect(() => { let isMounted = true; const fetchData = async () => { const data = await fetchUser(); if (isMounted) { // まだマウントされているときだけ更新する setUser(data); } }; fetchData(); return () => { isMounted = false; // アンマウット時にフラグを倒す }; }, []);
⑤ React StrictModeでは開発環境で2回実行される
React 18以降のStrict Modeでは、開発環境のみ useEffect が2回実行されます(意図的な設計)。本番環境では1回だけ実行されるので、クリーンアップが正しく書かれているか確認する機会として活用できます。
まとめ:気をつけること一覧
| 問題 | 原因 | 対策 |
|---|---|---|
| 古い値を参照する | 依存配列に値を入れ忘れ | ESLintの警告に従い依存配列を正しく書く |
| 無限ループ | オブジェクト・配列・関数を依存配列に入れた | useMemo / useCallback で参照を固定 |
| メモリリーク | タイマー・リスナーのクリーンアップ忘れ | return でクリーンアップ関数を返す |
| 非同期エラー | コールバックに直接 async を使った |
内部に async 関数を定義して呼び出す |
| アンマウント後の更新 | 非同期処理完了前にコンポーネントが消えた | isMounted フラグで更新を制御する |
useState・useRef・const との違いは「ReactのuseState・useRef・constの違い」を参照してください。