React · beginner
React maximum update depth exceeded: isolate the feedback loop
React's "Maximum update depth exceeded" error fires when a component schedules another render before the current render settles, usually because a render-phase side effect, dependency-driven effect, or setState-in-render pattern loops back into state. This guide isolates that feedback loop using the official useEffect semantics as the anchor for what counts as a side effect.
The symptoms
- •React throws "Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops."
- •Browser tab freezes or becomes unresponsive right after a component mounts, while other components on the page render normally.
- •React DevTools highlights one subtree where every render entry is followed by another render entry of the same component within microseconds, with state values oscillating between two values.
- •Production error monitoring surfaces a single component stack repeated dozens of times in one error report, all originating from the same hook callback or setState call site.
- •Adding a "fix" by guarding with a ref only hides the loop temporarily; the warning reappears as soon as the guarded branch is bypassed by a new code path.
Likely causes
- •Calling a setState (or a dispatcher that triggers one) directly inside the function body of the render, so every render schedules another render of the same component.
- •useEffect with a dependency that the effect itself writes to, so the effect retriggers every time it runs and keeps the depth budget burning.
- •A parent passes a fresh object or function reference on every render and the child puts it in a dependency array, so the child's effect runs after every parent render and posts state upward.
- •A reducer returns a new reference (array, object, function) on every dispatch even when the input action is identical, causing memoized consumers to think the state changed.
- •Conditional hook order or an early return before a useState/useEffect call makes the component render a different number of hooks on consecutive passes, which React treats as a state loop.
- •Two components each subscribe to the other's store slice via context; each subscription update re-renders the other, producing a stable ping-pong.
First ten minutes
- 01Open the stack trace from the thrown error and identify the exact component and hook that React blames; note whether the top frame is the component body, a useEffect callback, or a setState dispatcher.
- 02Open the file blamed by the top frame and locate the suspicious state hook and effect; do not change anything yet, just label them A (state) and B (effect).
- 03Read the dependency array of B and confirm whether A (or a value derived from A) appears in it; if it does, the effect is a candidate for a feedback loop with itself.
- 04Trace the call path from B to the setter for A: does the effect call a function whose return is passed to setA, and does that function read A from props or a closure?
- 05Add a temporary guard inside B that returns early when a ref counter exceeds 2, purely to confirm the loop hypothesis without changing behavior; remove it before finishing.
- 06If the warning vanishes with the guard, the cause is confirmed inside B; if not, suspect a render-phase setState in the parent of the blamed component instead.
Evidence to collect
- •The exact error message string including the word "Maximum update depth", plus the component stack printed by React above the message.
- •Names of the state setters and their corresponding useState/useReducer declarations in the blamed component and its immediate parent.
- •The dependency arrays of every useEffect, useMemo, and useCallback declared in the blamed component and its parent, written down exactly.
- •The values passed into those dependency arrays, with a note for each: is it a primitive from useState, a derived expression, or a prop?
- •The flow of data: which component owns the state, which component passes a callback or derived value down, and which component reads it back.
- •The render count of the blamed component during one user interaction, available from React DevTools' "rendered" counter or a temporary useEffect mount log.
Where to look
- •The function component boundary named in the React stack trace, starting at its top-level body and working down through every hook declaration.
- •Every useEffect in that component and in its direct parent; check whether each effect's dependencies are also written by the effect or by an immediate caller.
- •Custom hooks invoked by the blamed component; expand each one and treat it as if it were inline, since their dependencies are flattened into the same array.
- •Any setState call inside a render-phase helper such as a render-prop, a context provider's value prop, or a useMemo computation.
- •Context boundaries above the blamed component; if a provider recreates its value on every render, every consumer is a candidate to retrigger updates.
- •The reducer function used by useReducer if present; check whether it returns a referentially new object for actions that should be no-ops.
Diagnostic steps
- 01Confirm the error text contains "Maximum update depth exceeded"; if it instead says "Too many re-renders", the cause is usually a synchronous setState in an event handler rather than an effect loop, and the playbook diverges.
- 02Read the top frame of the stack: if it is a setState call inside a function component body (not inside an effect), the loop is render-phase setState, not an effect feedback loop.
- 03If the top frame is a useEffect callback, enumerate every value referenced inside that callback and compare each one against the dependency array; the loop candidate is any referenced value that is also listed.
- 04Inspect the setter called from inside the effect; follow it to the useState declaration and confirm whether the variable written into the setter is itself a dependency of the same effect.
- 05Check whether the effect calls a function passed in via props; if that function is a new reference on every render (inline arrow function or non-memoized callback), the dependency is unstable and the effect re-runs unnecessarily.
- 06Inspect the parent for any setState calls during render, such as in a context provider's value calculation or in a useMemo without a stable input; this produces a synchronous loop React cannot flatten.
- 07Reproduce the loop with the smallest possible parent: comment out optional children until only the blamed component and its direct ancestor remain, and confirm the loop still fires.
- 08If the loop disappears with the parent trimmed, the cause lives in the parent's data flow, not in the blamed component's hooks.
Common mistakes
- •Adding a ref-based guard to break the loop without understanding what writes the dependency, so the same dependency continues to bounce through other consumers.
- •Wrapping the offending setState in a setTimeout to "delay" it, which hides the synchronous loop React caught and produces a slower but still unbounded re-render stream.
- •Removing a dependency from the array to silence the warning without fixing the underlying cause, trading a feedback loop for stale-closure bugs.
- •Assuming the error always points at the deepest component in the stack; the actual loop is often one or two frames above the deepest frame.
- •Conflating "Maximum update depth exceeded" with "Too many re-renders" and applying the same fix; the former is an effect/render loop, the latter is a synchronous dispatcher loop in a handler.
- •Ignoring custom hooks inside the blamed component, which flatten their own dependencies into the call site and frequently hide the unstable reference.
Safe fixes
- •If the loop is a render-phase setState, move the call out of the component body into a useEffect with a stable dependency, or compute the value from existing state via useMemo instead of mirroring it.
- •If the loop is an effect that writes to one of its own dependencies, split the state so the written value lives in a ref updated imperatively, and keep only inputs the effect does not own in the dependency array.
- •If the loop is caused by an unstable callback prop, memoize the callback in the parent with useCallback and stabilize its inputs so the child's dependency array sees a stable reference.
- •If the loop comes from a reducer returning a fresh object for no-op actions, return the previous state reference unchanged when the action is a no-op, preserving referential equality.
- •If the loop comes from a context provider recreating its value each render, memoize the provider's value with useMemo against truly stable inputs so consumers do not see a new object each render.
- •If hook order changes between renders (early return before a useState or useEffect), hoist the conditional so the hook count is identical on every render path.
Prove the fix
- 01The "Maximum update depth exceeded" error no longer appears in the console or in production error reports for the same code path that previously triggered it.
- 02React DevTools shows the blamed component rendering exactly once per genuine state change and zero additional renders caused by its own effects in between.
- 03Manually exercising the previously failing interaction (mount, prop change, or store update) produces a render count that matches the number of distinct state transitions, with no back-to-back renders of the same component.
- 04Removing the temporary ref-based guard introduced during diagnosis no longer causes the warning to return, confirming the guard was not masking a real loop.
- 05A targeted regression test or interaction script reproduces the original user action and asserts that the component's render count stays within a small bounded number across the action.
Prevention and next steps
- •Keep useEffect dependency arrays honest: list every value the effect reads from the enclosing scope, and prefer storing values you only need to write imperatively in refs rather than state.
- •Use useCallback and useMemo only when the downstream consumer depends on referential stability; do not memoize by reflex, since it obscures the actual data flow.
- •Define an explicit contract for which component owns each piece of state and which component is allowed to write it; dual-ownership across parent and child is a common loop shape.
- •Treat any setState call inside a function component's body as a code smell unless it is the React-recommended pattern for derived state with a same-value bailout.
- •In code review, ask "does this effect write to one of its own dependencies?" before approving a useEffect; a "no" is the default expectation.
Safe commands and checks
grep -rn "useEffect" <component-path> grep -rn "useState" <component-path> grep -rn "useReducer" <component-path> grep -rn "useCallback" <component-path> grep -rn "useMemo" <component-path>