React 'Maximum Update Depth Exceeded' Fix
If your app has frozen, the tab is spiking to 100% CPU, and your console is flooding with Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render, you're looking at one of the most common runtime errors in React. It means a component is stuck re-rendering itself forever, and the browser eventually gives up. The good news: it almost always comes down to one of two patterns, and both are quick to fix once you know where to look.
What Causes Maximum Update Depth Exceeded
React throws this error when a state update inside a component triggers a re-render, and that re-render triggers the exact same state update again, with no condition that ever breaks the cycle. React caps the number of nested updates it will process before bailing out, which is why you see the error instead of a silently frozen tab in most cases.
There are two culprits behind the vast majority of reports of this error:
- Calling a state setter directly in the render body, instead of in an event handler or effect
- A
useEffecthook that updates state on every run, combined with a dependency array that changes on every render
Both produce the identical symptom: the component re-renders, the update fires again, and the loop never terminates.
Cause 1: Calling setState During Render
This is the fastest way to trigger the error, and it's an easy mistake when you're trying to derive one piece of state from another:
function SearchResults({ query }) {
const [count, setCount] = useState(0);
// BUG: this runs on every render, and setCount triggers another render
setCount(query.length);
return <p>{count} characters typed</p>;
}
Every render calls setCount, which schedules a new render, which calls setCount again. React has no way to know this will ever stop, so it throws after a fixed number of iterations.
The fix is almost always to stop treating derived values as state at all:
function SearchResults({ query }) {
// No state needed — just compute it during render
const count = query.length;
return <p>{count} characters typed</p>;
}
If the value genuinely needs to live in state (for example, it comes from an async source), move the update into an event handler or a properly-guarded useEffect, never directly in the function body.
Cause 2: A useEffect With an Unstable Dependency
The second common trigger looks like this:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const options = { userId }; // new object every render
useEffect(() => {
fetchUser(userId).then(setUser);
}, [options]); // options is a new reference every time
return <div>{user?.name}</div>;
}
options is a brand-new object on every render, so React's dependency comparison (which uses Object.is) always sees it as "changed." That reruns the effect, which sets state, which triggers a re-render, which creates a new options object, and the cycle repeats forever.
The fix is to depend on the primitive value that actually matters, not an object or array that gets recreated each render:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // primitive value — stable between renders unless it truly changes
return <div>{user?.name}</div>;
}
If you do need an object or function in the dependency array, wrap it in useMemo or useCallback so its reference stays stable between renders.
Step by Step: How to Track Down the Loop
- Open the browser console and find the component name in the stack trace — React usually points at the exact component that's looping.
- Search that component for any
setX(...)call that isn't inside auseEffect, event handler, or a condition — that's the most likely offender. - If the setter is already inside a
useEffect, check the dependency array line by line. Any object, array, or function created inline ({},[],() => {}, or a new object from props spreading) is a candidate for the loop. - Temporarily comment out the suspicious state update and reload. If the freeze stops, you've confirmed the source — now apply the fix above.
- Re-enable React's Strict Mode during development if it's off; it deliberately double-invokes effects and makes unstable dependencies show up faster in local testing.
Other Places This Error Hides
- Calling a handler instead of passing it:
onClick={handleClick()}invokes the function immediately during render instead of on click. IfhandleClicksets state, this reproduces the exact same loop. - Custom hooks that return a new object every call: if a custom hook returns
{ data, error }as a freshly-created object each time, and a consumer depends on that return value in its ownuseEffect, the loop can originate two files away from where the state update actually happens. - Context providers re-creating their
valueprop: a provider like<MyContext.Provider value={{ user, setUser }}>creates a new object every render, which can cascade into loops in any consumer that depends on that context value inside an effect.
Best Practices to Avoid This Error
- Never call a state setter directly in a component's render body — only in effects, event handlers, or callbacks.
- Keep
useEffectdependency arrays limited to primitives (strings, numbers, booleans) whenever possible. - Memoize objects and functions with
useMemo/useCallbackbefore putting them in a dependency array. - Enable the
react-hooks/exhaustive-depsESLint rule — it won't catch every unstable reference, but it catches the common missing-dependency mistakes that lead here. - Keep React's Strict Mode on in development so unstable effects surface locally instead of in production.
Frequently Asked Questions
Does this error only happen with useEffect?
No. It's most commonly triggered by useEffect, but it can come from any state update that isn't gated by a condition — including updates inside custom hooks, context providers, or even a useLayoutEffect.
Why does my app work fine locally but fail in production with this error? Strict Mode's double-invocation of effects in development can sometimes mask a loop that only becomes visible under different timing in production, or vice versa. Always test the specific component with Strict Mode enabled, and check the dependency array carefully rather than relying on "it worked locally."
Is this the same as the "Too many re-renders" error? They're closely related and often confused. "Too many re-renders" specifically points at a setState call happening synchronously during render (Cause 1 above), while "Maximum update depth exceeded" is the more general message that also covers effect-driven loops (Cause 2).
Can a third-party library cause this?
Yes — if a library's hook returns a new object or array reference on every call and you depend on it in your own useEffect, the loop originates in your code but the unstable reference comes from the library. Check the library's changelog or source for a memoization fix, or wrap the value yourself with useMemo before using it as a dependency.
Key Takeaways
Maximum update depth exceeded almost always traces back to a state update with no way to stop itself — either a setter called directly during render, or a useEffect whose dependency array changes on every pass. Fix it by moving derived values out of state entirely when possible, and by making sure every object or function in a dependency array has a stable reference across renders. Once you've fixed the specific loop, add the exhaustive-deps ESLint rule and keep Strict Mode on locally so the next one surfaces before it ships.
0 Comments
No comments yet — be the first to share your thoughts.