useEffectが無限ループになる理由と防止策:依存配列と副作用の構造をレビューで見抜く
useEffect
で何か処理をしたら、画面が更新され続けて止まらなくなった。
そんな経験がある開発者は少なくないはずです。
このような「無限ループ問題」は、依存配列の設定ミスや副作用設計の曖昧さが原因で起きます。レビューアーの視点では、依存配列に何を含めるか、含めないかの構造的意図を読み取ることが重要です。
典型例:state更新がトリガーとなるループ
ループ発生コード
useEffect(() => {
setCount(count + 1);
}, []);
Comment
@Reviewer: `count`が依存配列に含まれていないため、初回マウント時は問題ありませんが、`setCount`が状態更新→再レンダリング→再実行を誘発し、無限ループになります。
明示的な依存指定と暗黙的依存
ReactではuseEffect
の第2引数として依存配列を渡すことで、実行タイミングを制御します。
正しい依存設定
useEffect(() => {
if (count > 0) {
console.log('カウントが変化');
}
}, [count]);
依存配列を省略すると毎レンダリング時に実行され、
空配列 []
を渡せば「初回マウント時のみ実行」になります。
しかし、この明示的な制御が破綻すると、想定外の副作用が連鎖して止まりません。
関数依存による見えにくい再実行
NG例:関数を定義しつつ依存設定なし
useEffect(() => {
const fetchData = async () => {
const res = await fetch(url);
const json = await res.json();
setData(json);
};
fetchData();
}, []);
Comment
@Reviewer: `url` が依存配列に含まれていません。変化時に再フェッチされず、更新の伝播漏れになります。また関数定義は毎回新しい参照になるため、依存に含めないとメモリのズレを招きます。
改善案:useCallbackで安定化させる
const fetchData = useCallback(async () => {
const res = await fetch(url);
const json = await res.json();
setData(json);
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
useEffectの中でsetStateするときのルール
NG例:依存関係不一致
useEffect(() => {
if (user.isAdmin) {
setMessage('ようこそ管理者様');
}
}, []);
この例では user
の変化に追従できません。状態更新の起点に使っている値は依存に含めるのが基本です。
修正案
useEffect(() => {
if (user.isAdmin) {
setMessage('ようこそ管理者様');
}
}, [user]);
無限ループレビュー観点チェックリスト
図解:setStateによる再実行ループ
まとめ
無限ループは「コードミス」というよりも、構造設計の不備です。
状態変更が副作用を再実行し、それがまた状態を変えるという流れは、ループの温床になります。
レビューアーは、依存配列の設計と副作用内容をセットで見て、
再実行を誘発する条件がないかを常に構造的に評価することが求められます。
Reactのレンダリングは速くなっていても、無限ループの代償は軽くありません。
副作用設計はシンプルに、依存関係は明確に。これがループを防ぐ第一歩です。