緊急ブレーキ
Reactが緊急ブレーキをかけました。このエラーは通常、コンポーネントがループに陥り、Reactが停止させなければブラウザがフリーズするほどの速さで再レンダリングを繰り返していることを意味します。コンソールには赤いテキストの山が表示され、UIは反応しなくなるでしょう。
Error: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect...
要約:クイックフィックス
- 配列の欠落: コンポーネントのマウント時に一度だけエフェクトを実行したい場合は、第2引数として
[]を追加します。 - ステートのフィードバックループ: 依存関係でもあるステートを更新していませんか?
setCount(count + 1)の代わりに関数型アップデートsetCount(c => c + 1)に切り替えてください。 - 不安定な参照: 依存関係がコンポーネント内で定義されたオブジェクトや配列の場合、レンダリングごとに「新しい」オブジェクトになります。
useMemoでラップしてください。 - ガード句の使用:
setStateをif文で囲み、データが実際に変更されたときだけ実行されるようにします。
なぜこれが起こるのか
このエラーは安全網(セーフティネット)だと考えてください。Reactはレンダリングサイクルを監視しており、1秒間に50回以上の連続した更新を検出すると介入します。 useEffect において、これはほぼ間違いなく連鎖反応です:レンダリング → エフェクト → ステート変更 → 再レンダリング → エフェクト。
1. 依存配列の欠落
第2引数を忘れるのはよくあるミスです。これがないと、エフェクトはレンダリングのたびに実行されます。そのエフェクトがステートを更新すると、新しいレンダリングが強制され、再びエフェクトが実行されます。これは終わりのないサイクルです。
// ❌ 悪い例:無限ループ
useEffect(() => {
setUserData(data);
}); // ここに配列がない!
2. 参照等価性の罠
JavaScriptはオブジェクトや配列を値ではなく参照で比較します。簡単に言うと、 [] === [] は false です。コンポーネント内で定数オブジェクトを定義して依存配列に渡すと、Reactはコンポーネントがレンダリングされるたびに、それがまったく新しい依存関係であると判断します。
// ❌ 悪い例:'options' はレンダリングごとに新しいメモリ参照になります
const options = { theme: 'dark' };
useEffect(() => {
// ここにロジック
}, [options]);
解決方法
方法 1:関数型アップデートを使用する
更新のためだけに現在のステートを依存配列に含めるのはやめましょう。関数型アップデートを使用することで、依存関係リストからステート変数を完全に削除し、ループを断ち切ることができます。
// ❌ 悪い例:配列内の 'count' がループを引き起こします
useEffect(() => {
const timer = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(timer);
}, [count]);
// ✅ 良い例:'count' の依存関係は不要です
useEffect(() => {
const timer = setInterval(() => setCount(prev => prev + 1), 1000);
return () => clearInterval(timer);
}, []);
方法 2:useMemoでオブジェクトを安定させる
オブジェクトを依存関係にする必要がある場合は、レンダリング間でその参照が同じであることを保証してください。 useMemo はオブジェクトをキャッシュするため、Reactはその内部の値が実際に変わったときだけ「変更された」と認識します。
// ✅ 良い例:'theme' が変更された場合にのみ 'options' が変更されます
const options = useMemo(() => ({
color: theme === 'dark' ? '#fff' : '#000'
}), [theme]);
useEffect(() => {
// 特定のテーマが変更されたときのみロジックが実行されます
}, [options]);
方法 3:手動での比較
ステートの更新をトリガーする前に、実際に更新が必要かどうかを確認してください。この単純なガード句により、不要な再レンダリングが雪だるま式に増えてクラッシュするのを防げます。
useEffect(() => {
if (newId !== currentId) {
setCurrentId(newId);
}
}, [newId, currentId]);
修正の確認方法
- コンソールを確認する: ページをリフレッシュします。赤いエラーブロックが消えているはずです。
- ネットワークタブを確認する: エフェクトでデータを取得している場合、1秒間に数十件の同一リクエストが送信されていないか確認してください。1、2回なら正常ですが、50回はバグです。
- React Profiler: React DevToolsのProfilerを使用します。「Commit」タイムラインに、1つのコンポーネントに対する急速な更新の平坦な線が表示されている場合、ループはまだ残っています。
参考文献
- React ドキュメント: エフェクトとの同期
- React ドキュメント: 前のステートに基づいてステートを更新する
- MDN Web Docs: JavaScriptにおける等価比較と同一性

