更新: 2026年03月 · 11 分で読める · 5,253 文字
React useEffectで無限ループを防ぐ3つのパターン
useEffectの無限ループは、依存配列の設定ミスや状態更新のタイミングの誤解が原因です。この記事では、無限ループが発生する具体的な原因と、すぐに実装できる3つの解決パターンを紹介します。
useEffectの無限ループが起きる仕組み
React useEffectの無限ループは、以下のシナリオで発生します:
- 依存配列(dependency array)を指定しない
- 依存配列に毎回異なるオブジェクト参照を含める
- useEffect内で依存配列に含まれる状態を更新している
ブラウザの開発者ツールのコンソールで「Warning: useEffect has a missing dependency」というワーニングが出ていれば、ほぼ確実に無限ループの原因があります。
パターン1:依存配列を明示的に指定する
最も一般的な原因は、依存配列を省略することです。依存配列なしでは、レンダリングのたびにuseEffectが実行されてしまいます。
問題のあるコード
import React, { useState, useEffect } from 'react';
export const ProblematicComponent = () => {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
// ❌ 依存配列がない = 毎回実行される
useEffect(() => {
console.log('Effect running');
setData({ value: count });
// このコンポーネントのレンダリング → useEffect実行 → setData
// → 再レンダリング → useEffect実行 の無限ループ
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
解決策
import React, { useState, useEffect } from 'react';
export const FixedComponent = () => {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
// ✅ 依存配列を指定:countが変わった時だけ実行
useEffect(() => {
console.log('Effect running with count:', count);
setData({ value: count });
}, [count]); // 依存配列にcountを指定
return (
<div>
<p>Count: {count}</p>
<p>Data: {data?.value}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
依存配列にcountを指定することで、countが変わる時だけuseEffectが実行されます。初回マウント時に実行され、その後はcountが変わる度に実行される安定的な挙動になります。
パターン2:オブジェクト参照の変動を防ぐ
依存配列にオブジェクトを直接入れるのは避けてください。毎回新しいオブジェクト参照が作成され、値が同じでも「異なる」とみなされ、無限ループの原因になります。
問題のあるコード
export const ProblematicObjectComponent = () => {
const [data, setData] = useState(null);
// ❌ 毎レンダリング時に新しいオブジェクトが作成される
const config = { apiUrl: 'https://api.example.com' };
useEffect(() => {
console.log('Fetching with config:', config);
// APIコール処理
setData({ fetched: true });
}, [config]); // configは毎回新しい参照なので、常に実行される
return <div>{data?.fetched ? 'Loaded' : 'Loading'}</div>;
};
解決策1:useMemoでオブジェクトをメモ化
import React, { useState, useEffect, useMemo } from 'react';
export const FixedWithMemoComponent = () => {
const [data, setData] = useState(null);
// ✅ useMemoでオブジェクト参照を安定化
const config = useMemo(() => ({
apiUrl: 'https://api.example.com'
}), []); // 依存配列が空 = 初回マウント時のみ作成
useEffect(() => {
console.log('Fetching with config:', config);
setData({ fetched: true });
}, [config]); // configの参照が安定しているので、マウント時のみ実行
return <div>{data?.fetched ? 'Loaded' : 'Loading'}</div>;
};
解決策2:プリミティブ値を依存配列に含める
export const FixedWithPrimitivesComponent = () => {
const [data, setData] = useState(null);
const [apiUrl] = useState('https://api.example.com');
// ✅ オブジェクトではなく、必要なプリミティブ値のみ依存配列に含める
useEffect(() => {
console.log('Fetching from:', apiUrl);
setData({ fetched: true });
}, [apiUrl]); // 文字列は参照の心配がない
return <div>{data?.fetched ? 'Loaded' : 'Loading'}</div>;
};
パターン3:クリーンアップ関数で状態更新を制御する
非同期処理後の状態更新が無限ループの原因になることがあります。コンポーネントがアンマウントされた後の状態更新を防ぎましょう。
問題のあるコード
export const ProblematicAsyncComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
// ❌ 非同期処理中にコンポーネントがアンマウントされても
// setDataが実行される可能性がある
fetch('https://api.example.com/data')
.then(res => res.json())
.then(json => {
setData(json); // ここでアンマウント後に実行されるとメモリリーク警告
setError(null);
})
.catch(err => setError(err));
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading'}</div>;
};
解決策:クリーンアップ関数とフラグを使用
export const FixedAsyncComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
// ✅ マウント状態を追跡するフラグを作成
let isMounted = true;
fetch('https://api.example.com/data')
.then(res => res.json())
.then(json => {
if (isMounted) {
setData(json);
setError(null);
}
})
.catch(err => {
if (isMounted) {
setError(err);
}
});
// クリーンアップ関数:アンマウント時にフラグを変更
return () => {
isMounted = false;
};
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading'}</div>;
};
実装時の推奨プラクティス
useCallbackとの組み合わせ
関数を依存配列に含める場合、useCallbackでメモ化することで無限ループを防げます。
import React, { useState, useEffect, useCallback } from 'react';
export const WithCallbackComponent = () => {
const [data, setData] = useState(null);
// ✅ useCallbackで関数を安定化
const fetchData = useCallback(async () => {
const response = await fetch('https://api.example.com/data');
const json = await response.json();
setData(json);
}, []);
useEffect(() => {
fetchData();
}, [fetchData]); // fetchDataの参照が安定している
return <div>{data ? 'Data loaded' : 'Loading'}</div>;
};
デバッグのヒント
無限ループが疑われる場合、以下のデバッグテクニックが有効です:
useEffect(() => {
console.log('Effect executed at:', new Date().toISOString());
console.trace(); // スタックトレース表示で呼び出し元を追跡
// 処理...
}, [/* 依存配列 */]);
コンソールに短時間に大量のログが出力されれば、無限ループが確定です。
使うべき場面と避けるべき場面
| 使うべき場面 | 避けるべき場面 |
|---|---|
| APIデータ取得(マウント時のみ) | 毎フレーム実行が必要な処理 |
| 特定の状態に応じた副作用 | 同期的な計算処理 |
| イベントリスナー登録 | レンダリングに直結する値の計算 |
よくある質問
依存配列を空配列[]にすると、マウント時のみuseEffectが実行され、再レンダリング時には実行されません。一方、依存配列を省略すると、毎回のレンダリング時に実行されます。ほぼすべての場合、空配列か特定の値を指定すべきです。
useReducerはuseEffectの依存配列の問題を根本的には解決しません。ただし、複雑な状態更新ロジックの場合、useReducerで状態管理を明確化することで、無限ループの原因を特定しやすくなります。
まとめ
- useEffectの無限ループは、依存配列の指定ミスや参照の変動が主な原因
- 必ず依存配列を明示的に指定し、必要な値だけを含める
- オブジェクト参照の変動を防ぐため、useMemoやuseCallbackを活用
- 非同期処理後の状態更新は、クリーンアップ関数でマウント状態を管理
- ESLintの「exhaustive-deps」ルールに従い、意図的な除外は明記する
これら3つのパターンをマスターすれば、useEffectの無限ループはほぼ確実に防げます。実装時は、依存配列の内容をコンポーネントの意図と照らし合わせることが重要です。