React の useEffect で無限ループが止められない
useEffect の依存配列に毎レンダリングで参照が変わるオブジェクトや関数を入れると、setState → 再レンダリング → 依存変化 → 再実行が連鎖して無限ループになる。
プリミティブに分解し、useMemo / useCallback で参照を安定化し、関数型 setState で依存を切る。
#react#useeffect#hooks#infinite-loop#dependency-array
要約
useEffect の無限ループは 依存配列に毎レンダリングで参照が変わる値が含まれている か、Effect 内で setState した値そのものを依存に入れている のが主因。
プリミティブに絞る、useMemo / useCallback で参照を安定化する、関数型 setState で依存を切る、の 3 手で大半は止まる。
よくある原因
- オブジェクト・配列・関数リテラルを依存に入れている:
useEffect(() => {...}, [{ id: 1 }])のような書き方は毎レンダリングで新しい参照になり、必ず再実行される。 - setState した値を依存にしている: Effect 内で
setCount(count + 1)のように更新し、[count]を依存に置くと自己再起ループする。 - 依存配列を渡し忘れ:
useEffect(() => {...})と書くと毎レンダリングで実行される。
中で state を更新すれば即無限ループ。 - 親 props の参照が毎回新しい: 親が
<Child options={{ a: 1 }} />のようにレンダリングのたびに新しいオブジェクトを渡してくる。
解決策
1. プリミティブに分解する
// NG
useEffect(() => {
fetchUser(user);
}, [user]); // user は毎回新しいオブジェクト
// OK
useEffect(() => {
fetchUser({ id: userId });
}, [userId]); // 文字列で安定公式の useEffect リファレンス でも依存はプリミティブ推奨。
2. useMemo / useCallback で参照を安定化
const options = useMemo(() => ({ a, b }), [a, b]);
const handleClick = useCallback(() => doIt(id), [id]);
useEffect(() => {
subscribe(options, handleClick);
}, [options, handleClick]);3. 関数型 setState で依存を切る
// NG: count を依存に入れる必要が出てしまう
useEffect(() => {
const t = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(t);
}, [count]);
// OK: 関数型なら count が依存から外せる
useEffect(() => {
const t = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(t);
}, []);4. Strict Mode の二重実行と切り分ける
開発モードでは <React.StrictMode> 配下の Effect がマウント時に 2 回走る仕様。
これは無限ではないので、コンソール出力が「初回 2 回だけ」なら別問題と判断する。