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

  1. 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.
  2. 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).
  3. 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.
  4. 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?
  5. 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.
  6. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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

  1. 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.
  2. 02React DevTools shows the blamed component rendering exactly once per genuine state change and zero additional renders caused by its own effects in between.
  3. 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.
  4. 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.
  5. 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>