エラーの内容
Too many re-renders. React limits the number of renders to prevent an infinite loop.
Reactは一定のレンダー回数(開発環境では約25回)に達するとレンダリングを中断します。アプリがフリーズし、コンソールに同じエラーが大量に表示され、ブラウザのタブがクラッシュすることもあります。ほぼ必ずと言っていいほど、コンポーネントが毎回のペイントで自身の再レンダーをスケジュールしていることが原因です。
根本原因
ほぼすべてのケースは以下の3つのパターンのいずれかが原因です:
- ハンドラーやエフェクトの外で、レンダーボディ内でstateのsetterを直接呼び出している
- イベントプロップに関数の参照ではなく実行済みの関数を渡している
useEffectの依存関係がレンダーごとに変化し、ループが発生している
修正1:レンダー中にstateのsetterを呼び出している
最もよくある原因です。setterが発火して再レンダーを引き起こし、また発火する、という繰り返しになります。ブラウザが落ちる前に、Reactは約25回でこれを強制終了します。
// ❌ 間違い — setCountはレンダーのたびに実行される
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // レンダーボディ内での直接呼び出し
return <div>{count}</div>;
}
// ✅ 修正 — useEffectかイベントハンドラーの中に移動する
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // レンダー中ではなく、マウント後に実行される
}, []); // 依存配列が空 = 一度だけ実行
return <div>{count}</div>;
}
修正2:関数を渡す代わりに呼び出してしまっている
たった一組の括弧が原因です。onClick={handleClick()}と書くと、レンダー中に関数が即座に実行されます。onClick={handleClick}と書くと、参照として渡され、クリック時にReactが呼び出します。
// ❌ 間違い — handleClick()はクリック時ではなくレンダー中に実行される
function MyButton() {
const [open, setOpen] = useState(false);
function handleClick() {
setOpen(true);
}
return <button onClick={handleClick()}>Open</button>;
}
// ✅ 修正 — 括弧なしで参照を渡す
return <button onClick={handleClick}>Open</button>;
引数を渡す必要がある場合は、代わりにアロー関数でラップします:
// ✅ アロー関数 — 安全で、レンダー時に実行されない
return <button onClick={() => handleClick(someId)}>Open</button>;
修正3:useEffectの依存関係ループ
自身の依存関係を変更するエフェクトは永遠に実行し続けます。stateが変わる → エフェクトが再実行される → stateがまた変わる、という流れです。依存配列からそのstate変数を取り除くことでループを断ち切れます。
// ❌ 間違い — itemsが変化 → エフェクト実行 → itemsが変化 → ループ
useEffect(() => {
setItems([...items, newItem]);
}, [items]);
// ✅ 修正 — 関数型アップデーターを使えば依存配列にstateを入れずに最新値を参照できる
useEffect(() => {
setItems(prev => [...prev, newItem]);
}, [newItem]); // newItemが変化したときだけ再実行される
修正4:依存関係にインラインオブジェクトや配列を使っている
これは見落としがちなパターンです。JavaScriptはレンダーのたびに新しいオブジェクト参照を生成します。内部の値が同じでも同様です。Reactは依存関係のチェックに参照の同一性を使うため、インラインで書かれた{ id: 1 }は常に「変化した」と見なされます。
// ❌ 間違い — { id: 1 }はレンダーのたびに新しい参照になる
useEffect(() => {
fetchData({ id: 1 });
}, [{ id: 1 }]);
// ✅ 修正 — useMemoで参照を安定させ、レンダー間で同じ参照を保持する
const params = useMemo(() => ({ id: 1 }), []);
useEffect(() => {
fetchData(params);
}, [params]);
修正5:止まらない条件付きsetState
ifブロック内でstateをセットするのは無害に見えますが、条件が常にtrueの場合、レンダーのたびに発火します。
// ❌ 間違い — dataが存在する限り条件は常にtrue
function Form({ data }) {
const [value, setValue] = useState('');
if (data) {
setValue(data.name); // dataが存在する限りレンダーのたびに実行される
}
return <input value={value} />;
}
// ✅ 修正 — dataを依存関係としてuseEffect内で同期する
function Form({ data }) {
const [value, setValue] = useState('');
useEffect(() => {
if (data) {
setValue(data.name);
}
}, [data]); // dataが実際に変化したときだけ実行される
return <input value={value} />;
}
ループしているコンポーネントを特定する方法
Reactのスタックトレースは自分のファイルではなく、内部のfiberコードを指していることがよくあります。実際の原因を特定するには以下の手順を試してください:
- React DevToolsを開く → Profilerタブ → Recordを押す → ループしているコンポーネントは1秒以内に50〜200回レンダーされていることがわかる
- 怪しいコンポーネントの先頭に
console.count('MyComponent')を追加する — 100回以上表示されているものが犯人 useEffectフックを一つずつコメントアウトして、ループが止まるものを探す — それが原因- すべてのイベントプロップ(
onClick、onChange、onSubmit)を確認し、ハンドラー名の後に誤って()がついていないかチェックする
修正の確認
- コンソールからエラーが消える
- React DevTools Profilerで、コンポーネントがユーザー操作や親の再レンダー時にのみレンダーされ、自発的にはレンダーされないことを確認する
- 初回マウント後に
console.countのカウントが増えなくなる - NetworkタブでAPIコールがアクション1回につき1回だけ発火し、連続して発生しないことを確認する
予防チェックリスト
- stateのsetterはイベントハンドラーまたはエフェクト内に置く — レンダーボディ内には絶対に書かない
- イベントプロップには参照を渡す:
onClick={fn}(呼び出しonClick={fn()}はNG) useEffect内では関数型アップデーター形式(prev => ...)を優先し、依存配列からそのstate変数を除く- エフェクトの依存関係として使うオブジェクトや配列は
useMemoやuseCallbackでラップし、参照を安定させる - プロジェクトに
eslint-plugin-react-hooksを追加する —exhaustive-depsルールがブラウザに届く前にこれらの問題のほとんどを検出してくれる

