React useEffectの無限ループ、なぜ?5つの原因と対策まとめ
useEffectの無限ループ、一度はハマったことありませんか?
私たちWebエンジニアにとって、Reactの開発現場でuseEffectが原因の謎の再レンダリング――「気付いたら画面が固まる」「コンソールがエラーで埋まる」現象は“お約束”とも言える落とし穴です。
その場しのぎの// eslint-disable-lineで乗り切っても、また同じバグが顔を出す…。
そんな“技術的モヤモヤ”、一緒に論理的に解消しましょう。
(Reactの基本的なフックの違いについては『【保存版】ReactのuseStateとuseEffectの違いとは?初心者が実務で迷わない使い分け完全ガイド』をご参照ください)
なぜ無限ループ? 根本原因は「参照の同一性」
ReactのuseEffectが「なぜか毎回走る」原因、キーワードは「参照の同一性」です。
- プリミティブ型(
string,numberなど)は“値”で比較 - オブジェクト型(
Object,Array,Function)は“参照(アドレス)”で比較
useEffect(() => { }, [obj]); のようにオブジェクトや関数を依存配列に入れると、毎レンダリングごとに「新しい参照」扱いとなり、無限ループの原因に。
- コンポーネントが再レンダリング
- 毎回「新しい参照」と判断される
- useEffectが再実行
- state更新がトリガー
- 1.に戻る…
この悪循環=「無限ループ」です。
(Reactの依存配列や再レンダリングの仕組みについては『【保存版】ReactのuseStateとuseEffectの違いとは?初心者が実務で迷わない使い分け完全ガイド』も参考になります)
用語解説:React
Facebookが開発したUI構築のためのJavaScriptライブラリ。コンポーネント単位で効率的にWebアプリを開発できる。用語解説:useEffect
Reactの関数コンポーネントで副作用(データ取得やDOM操作など)を扱うためのフック。依存配列によって実行タイミングを制御できる。用語解説:依存配列
useEffectの第2引数。ここに指定した値が変化したときだけeffectが再実行される仕組み。用語解説:参照の同一性
JavaScriptではオブジェクトや配列・関数は「中身」ではなく「参照(アドレス)」で比較される。新しく生成されるたびに異なる参照とみなされる。用語解説:プリミティブ型
string, number, boolean などの基本データ型。値そのもので比較される。用語解説:オブジェクト型
Object, Array, Function など。参照(アドレス)で比較されるため、毎回新しく生成すると異なるものと判定される。
よくある5つの無限ループパターンと解決策
1. 依存配列にオブジェクトや配列を直接指定
現象
propsやローカル定義のオブジェクト・配列をそのまま依存配列に入れると、参照が毎回変わりループ発生。
NG例
useEffect(() => {
// optionsが毎回新しくなる
}, [options]);
対策:useMemoでメモ化
const options = useMemo(() => ({ a: 1, b: 2 }), []);
useEffect(() => {
// optionsの参照が保たれる
}, [options]);
props渡しも、親側でuseMemoを使うのが安全策。
(JavaScriptのthisや参照の違いについては『JavaScript thisの違い・バグ事例7選|アロー関数やbindの迷わない使い分け』もご参照ください)
用語解説:useMemo
計算結果やオブジェクト・配列を「メモ化」し、依存値が変わらない限り同じ参照を再利用するReactのフック。
2. コンポーネント内で関数を定義&依存指定
現象
コンポーネント内で関数を宣言し、それを依存配列に指定。レンダリングごとに関数参照が変わりループ。
用語解説:useCallback
関数を「メモ化」し、依存値が変わらない限り同じ参照を再利用するReactのフック。無駄な再生成や再レンダリングを防ぐ。
NG例
const fetchData = () => { ... };
useEffect(() => { fetchData(); }, [fetchData]);
対策:useCallbackで関数をメモ化
const fetchData = useCallback(() => { ... }, [依存値]);
useEffect(() => { fetchData(); }, [fetchData]);
3. useEffect内でstateを更新し、依存配列にそのstateを指定
現象
setState対象を依存配列に入れ、effect内で更新 → ループ突入。
NG例
useEffect(() => {
setCount(count + 1);
}, [count]);
対策:依存配列の見直し or “関数型”更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
4. useEffectコールバックに直接async/await
現象 useEffect(async () => {...}, [])はPromiseを返すため非推奨。
NG例
useEffect(async () => {
const data = await fetchData();
setData(data);
}, []);
対策:effect内で別途async関数を定義・呼び出し
useEffect(() => {
const fetch = async () => {
const data = await fetchData();
setData(data);
};
fetch();
}, []);
5. 依存配列を省略している
現象
依存配列がない場合、effectは全レンダリングで毎回発火
state更新が絡むと、そのままループ
NG例
useEffect(() => {
setCount(c => c + 1);
}); // ←依存配列なし
対策:必ず「意図した依存のみ」指定
- 初回だけ実行:
[] - 特定値だけ監視:
[userId]
FAQ:「eslint-disable-lineでごまかし」本当に危険?
-
Q:
// eslint-disable-lineで警告を消すのはアリ?
A:非推奨。警告は依存指定漏れ=設計不備のサインです。
値はuseMemo、関数はuseCallbackで“参照の同一性”を担保し、警告を無視しないことがバグ予防につながります。用語解説:eslint
JavaScriptやReactのコード品質やバグを自動検出するための静的解析ツール。ルール違反時に警告やエラーを表示する。 -
Q:
useMemoとuseCallbackの違いは?
A:- useMemo:値のメモ化(例:オブジェクト、配列、計算結果)
- useCallback:関数そのもののメモ化
依存配列に「値」ならuseMemo、「関数」ならuseCallbackを。
用語解説:メモ化
計算結果や関数・オブジェクトを一時的に保存し、同じ依存値の間は再利用する最適化手法。無駄な再計算や再生成を防ぐ。
まとめ:無限ループに怯えず、論理で解決を
- 根本原因は「参照の同一性」
- 主な解決策は
useMemoとuseCallbackで参照を固定 - 依存配列は設計の要。「警告を無視しない」習慣がバグを防ぐ
まずは手元のコードで、依存配列の扱い・メモ化の使い分けを意識的に試してみましょう。
ぜひコードをコピペして、まずは動かしてみてください。
(Reactやフロントエンドフレームワークの選び方については『フロントエンドフレームワーク徹底比較!React・Vue・Angular違いと選び方』もご参照ください)