できない.dev

React で「Maximum update depth exceeded」が解決できない

レンダリングのたびに state 更新が走ると再レンダリングが連鎖し Maximum update depth exceeded になる。
ハンドラの即時呼び出し、render 中の setState、useEffect の依存設計を見直す。

#react#setstate#render#infinite-loop#hooks

要約

Maximum update depth exceeded は、レンダリングのたびに state 更新が発生し、再レンダリングが無限に連鎖 したときに React が止める警告。
原因はほぼ 3 つ — イベントハンドラの即時呼び出し、render 中の setStateuseEffect の依存設計ミス。
どれも「更新を呼ぶタイミング」の問題に集約される。

よくある原因

  1. ハンドラの即時呼び出し: onClick={setOpen(true)} のように、関数参照ではなく実行結果を渡している
  2. render 中の setState: コンポーネント本体で直接 setState を呼び、毎レンダリングで更新が走る
  3. useEffect の自己再起: Effect 内で更新した state を依存配列に入れ、更新 → 再実行を繰り返す
  4. 参照の作り直し: 親が毎回新しい関数 / オブジェクトを子に渡し、子の更新が連鎖する

解決策

1. ハンドラは渡す、呼ばない

// NG: レンダリング毎に実行され setState が走る
<button onClick={setOpen(true)}>open</button>
 
// OK: 関数参照を渡す
<button onClick={() => setOpen(true)}>open</button>

イベントに渡すのは「呼び出し方」であって「呼び出した結果」ではない。
考え方は 公式の Responding to Events を参照。

2. render 中に setState しない

// NG: 毎レンダリングで呼ばれ無限ループ
function App() {
  const [n, setN] = useState(0);
  setN(n + 1);
  return <p>{n}</p>;
}

初期値は useState の引数(必要なら遅延初期化)で与え、副作用は useEffect に閉じ込める。setState の挙動は useState 公式リファレンス

3. useEffect の依存を切る

// NG: count を依存に入れると更新で再実行が連鎖
useEffect(() => {
  setCount(count + 1);
}, [count]);
 
// OK: 関数型にして count を依存から外す
useEffect(() => {
  setCount(c => c + 1);
}, []);

4. 子へ渡す参照を安定化する

const handleClick = useCallback(() => doIt(id), [id]);

毎レンダリングで新しい関数を作って子に渡すと、子側の更新が連鎖することがある。useCallback / useMemo で参照を固定し、不要な再レンダリングの連鎖を断つ。

この記事は役立ちましたか?