"Maximum update depth exceeded."
Three causes, three fixes.
React stops after 50 nested updates and throws. The browser freezes for a second, then the console fills with warnings. This error means your component is causing renders faster than React can complete them — an infinite loop. There are exactly three causes, and each has a distinct fix. This page covers all three.
Short answer: Find which component is looping (React's stack trace tells you). Then check for three patterns: (1) setState in render — move to an event handler or useEffect. (2) unstable dep in useEffect — wrap functions in useCallback, objects in useMemo. (3) effect sets state that's in its own deps — use functional setState, add a guard, or move the value to a ref. Once you fix the cause, the loop stops.
✅ Solution 1 — setState in render
The most obvious cause — and the easiest to fix.
If you call setState during render, React schedules an update before the render finishes, which triggers a new render, which calls setState again — infinite loop. The fix: move the setState out of render, into an event handler or useEffect. Or, better, remove the state entirely and compute the value.
import { useState } from 'react'; // ─── BAD: setState in render ───────────────── function BadCounter() { const [count, setCount] = useState(0); setCount(count + 1); // ⚠️ Maximum update depth exceeded // Render → setState → re-render → setState → ... return{count}
; } // ─── GOOD: setState in an event handler ────── function GoodCounter() { const [count, setCount] = useState(0); function increment() { setCount(c => c + 1); // ✅ called from a click } return ; } // ─── GOOD: derived value, no state ─────────── function DerivedExample() { const [count, setCount] = useState(0); // If a value is derived, compute it in render. // Don't store it in state — that causes the loop. const doubled = count * 2; // ✅ derived, no loop return (); } // ─── BAD variant: setState inside .map() ───── function BadItems({ items }: { items: string[] }) { const [valid, setValid] = useState<string[]>([]); items.forEach(item => { if (item.length > 0) { setValid(v => [...v, item]); // ⚠️ setState during render } }); returnCount: {count}, Doubled: {doubled}
- {valid.map(v =>
- {v} )}
- {valid.map(v =>
- {v} )}
import { useState } from 'react'; function BadCounter() { const [count, setCount] = useState(0); setCount(count + 1); return React.createElement('p', null, count); } function GoodCounter() { const [count, setCount] = useState(0); function increment() { setCount(c => c + 1); } return React.createElement('button', { onClick: increment }, 'Count: ', count); } function DerivedExample() { const [count, setCount] = useState(0); const doubled = count * 2; return React.createElement('div', null, React.createElement('p', null, 'Count: ', count, ', Doubled: ', doubled), React.createElement('button', { onClick: () => setCount(c => c + 1) }, '+1') ); } function BadItems({ items }) { const [valid, setValid] = useState([]); items.forEach(item => { if (item.length > 0) { setValid(v => [...v, item]); } }); return React.createElement('ul', null, valid.map(v => React.createElement('li', { key: v }, v))); } function GoodItems({ items }) { const valid = items.filter(item => item.length > 0); return React.createElement('ul', null, valid.map(v => React.createElement('li', { key: v }, v))); } console.log(BadCounter, GoodCounter, DerivedExample, BadItems, GoodItems);
setState ≠ render
Render is for computing JSX. setState schedules a new render. Calling it during render creates a loop.
Event handlers are safe
onClick, onChange — these run after render. setState there schedules one render.
Derived values
If a value comes from other state, compute it in render. const doubled = count * 2. Don't store it.
Filtering/mapping
Do it during render. items.filter(...) in render is fine — it's not state, it's a computed value.
✅ Solution 2 — Unstable dependency in useEffect
The most common cause in real codebases.
When a useEffect has a dependency whose identity changes on every render, the effect re-runs on every render. If the effect also updates state, you get a loop: render → effect → setState → render → effect → setState → ... The fix: stabilize the dependency with useCallback, useMemo, or destructuring.
import { useState, useEffect, useCallback, useMemo } from 'react'; // ─── BAD: object dep causes loop ───────────── function BadObjectDep() { const [data, setData] = useState(0); const options = { retries: 3 }; // new object every render useEffect(() => { setData(d => d + 1); }, [options]); // ⚠️ Effect runs every render (options changed) // Effect calls setData → renders again → options changes → loop return{data}
; } // ─── GOOD: useMemo stabilizes object ───────── function GoodObjectDep() { const [data, setData] = useState(0); const options = useMemo(() => ({ retries: 3 }), []); // ✅ stable useEffect(() => { setData(d => d + 1); }, [options]); // ✅ Effect runs once (options stable) // Effect calls setData → renders again → options unchanged → no loop return{data}
; } // ─── BAD: function dep causes loop ─────────── function BadFunctionDep() { const [data, setData] = useState(0); const fetchData = () => { setData(d => d + 1); }; useEffect(() => { fetchData(); }, [fetchData]); // ⚠️ Loop — fetchData is new on every render return{data}
; } // ─── GOOD: useCallback stabilizes function ─── function GoodFunctionDep() { const [data, setData] = useState(0); const fetchData = useCallback(() => { setData(d => d + 1); }, []); // ✅ stable useEffect(() => { fetchData(); }, [fetchData]); // ✅ Effect runs once return{data}
; } // ─── GOOD: inline the function instead ─────── function InlineFix() { const [data, setData] = useState(0); useEffect(() => { // No external function, no dependency setData(d => d + 1); }, []); // ✅ runs once on mount return{data}
; } console.log(BadObjectDep, GoodObjectDep, BadFunctionDep, GoodFunctionDep, InlineFix);
import { useState, useEffect, useCallback, useMemo } from 'react'; function BadObjectDep() { const [data, setData] = useState(0); const options = { retries: 3 }; useEffect(() => { setData(d => d + 1); }, [options]); return React.createElement('p', null, data); } function GoodObjectDep() { const [data, setData] = useState(0); const options = useMemo(() => ({ retries: 3 }), []); useEffect(() => { setData(d => d + 1); }, [options]); return React.createElement('p', null, data); } function BadFunctionDep() { const [data, setData] = useState(0); const fetchData = () => { setData(d => d + 1); }; useEffect(() => { fetchData(); }, [fetchData]); return React.createElement('p', null, data); } function GoodFunctionDep() { const [data, setData] = useState(0); const fetchData = useCallback(() => { setData(d => d + 1); }, []); useEffect(() => { fetchData(); }, [fetchData]); return React.createElement('p', null, data); } function InlineFix() { const [data, setData] = useState(0); useEffect(() => { setData(d => d + 1); }, []); return React.createElement('p', null, data); } console.log(BadObjectDep, GoodObjectDep, BadFunctionDep, GoodFunctionDep, InlineFix);
Objects have identity
{ retries: 3 } creates a new object each render — different reference. useMemo returns the same reference until deps change.
Functions have identity
Same for functions. useCallback(fn, []) returns the same function until deps change.
Inline vs stabilize
If the function is only used in the effect, move it inside — no dependency. useCallback is for functions that must be passed to other components.
Primitives don't need memo
Strings, numbers, booleans — count, id, enabled — are compared by value. Adding them to deps is fine.
✅ Solution 3 — Effect sets state that's in its own deps
The subtlest cause — and the most important to know.
The pattern: useEffect(() => setX(...), [x]). The effect reads x, sets x, which changes x, which re-runs the effect — infinite loop. The fix depends on what the effect is trying to do. Often, the state doesn't need to be in useState at all — it's a derived value.
import { useState, useEffect, useRef } from 'react'; // ─── BAD: effect depends on state it sets ──── function BadSelfDep() { const [items, setItems] = useState<string[]>([]); useEffect(() => { // Reads items, sets items — infinite loop setItems(prev => [...prev, 'new item']); }, [items]); // ⚠️ Maximum update depth exceeded return
- {items.map(i =>
- {i} )}
- {source.map(i =>
- {i} )}
- {items.map(i =>
- {i} )}
History: {history.join(', ')}
; } // ─── GOOD 4: run once, use setInterval ─────── function IntervalFix() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); // functional — no count dep }, 1000); return () => clearInterval(id); }, []); // ✅ runs once on mount return{count}
; } console.log(BadSelfDep, DerivedFix, GuardedFix, RefFix, IntervalFix);import { useState, useEffect, useRef } from 'react'; function BadSelfDep() { const [items, setItems] = useState([]); useEffect(() => { setItems(prev => [...prev, 'new item']); }, [items]); return React.createElement('ul', null, items.map(i => React.createElement('li', { key: i }, i))); } function DerivedFix({ source }) { return React.createElement('ul', null, source.map(i => React.createElement('li', { key: i }, i))); } function GuardedFix() { const [items, setItems] = useState([]); useEffect(() => { if (items.length >= 3) return; const timer = setTimeout(() => { setItems(prev => [...prev, item-${prev.length}`]); }, 1000); return () => clearTimeout(timer); }, [items]); return React.createElement('ul', null, items.map(i => React.createElement('li', { key: i }, i))); } function RefFix({ value }) { const [history, setHistory] = useState([]); const lastValue = useRef(value); useEffect(() => { if (lastValue.current !== value) { lastValue.current = value; setHistory(h => [...h, value]); } }, [value]); return React.createElement('p', null, 'History: ', history.join(', ')); } function IntervalFix() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []); return React.createElement('p', null, count); } console.log(BadSelfDep, DerivedFix, GuardedFix, RefFix, IntervalFix);
Don't store derived
If x comes from other state, don't put it in useState. Compute during render. Eliminates the need for the effect entirely.
Functional setState + guard
setX(prev => ...) avoids stale closure. The guard (if (x.length >= 3) return) stops the loop conditionally.
Ref for "last value"
useRef stores the previous value without triggering renders. if (ref.current !== val) guards the setState.
Intervals and timers
setInterval + functional setState + empty deps = no loop, cleanup on unmount. The idiomatic pattern.
Don't silence the error by removing the dependency array. Removing deps means the effect runs every render — which is exactly the infinite loop, just without React catching it. Adding // eslint-disable-next-line hides the warning but keeps the loop. The fix is always to address the cause: don't set state in render, stabilize the dependency, or use functional setState with a guard. Every workaround trades a clear error for a silent bug.
⚠️ Common mistakes
The loop patterns that cost real time.
Function body, inside .map(), inside .forEach(). Fix: move to event handler or effect, or compute during render.
New identity every render → effect re-runs. Fix: useMemo, or destructure primitives from it.
Same — new function every render. Fix: useCallback, or inline inside the effect.
useEffect(() => setX(x + 1), [x]) — infinite loop. Fix: functional setState + guard, or derive instead.
setX(newValue) where newValue !== x — always triggers. Fix: if (x !== newValue) setX(newValue), or use functional setState.
const [prev, setPrev] = useState(...) — same problem. Fix: use useRef for previous values.
Effect runs every render — same loop, no error message. Fix: address the dependency properly.
Custom stores that notify on every change — the component re-renders, the store notifies, loop. Fix: useSyncExternalStore or guarded subscriptions.
❓ Frequently asked
What does "Maximum update depth exceeded" mean in React?
A component is triggering setState during render, in a useEffect with unstable dependencies, or in an event that re-fires itself, causing React to schedule renders faster than it can complete them. React stops after 50 nested updates and throws.
How do I find which useEffect is causing the loop?
React's error message includes a component stack trace in the console — search upward to find the effect. Alternatively, add console.log to each useEffect and see which one fires repeatedly. Or use React DevTools Profiler — the flame graph will show the same component rendering dozens of times.
Why does adding a function to useEffect dependencies cause a loop?
A function defined in a component body is a new object on every render — its reference changes each time the component renders. If it's in the dependency array, the effect thinks the dependency changed, so it re-runs. If the effect calls setState, that triggers another render, which creates a new function, which triggers the effect again — infinite loop.
How do I stop an infinite loop in useEffect?
Identify the cause: (1) setState in render — move it. (2) Function in deps — wrap with useCallback. (3) Object/array in deps — wrap with useMemo or destructure primitives. (4) Effect calls setState with a value that changes — use functional setState or add a guard. (5) setState unconditionally in an effect with the state in its own dependency array — add a condition or move to a ref.