React's maximum update depth exceeded error is its way of saying your component is caught in an infinite render loop. A state update happens, the component re-renders, another state update fires, and the cycle repeats until React hits a hard limit and crashes. This usually means your component is calling setState directly inside the render phase, or a dependency is missing from a useEffect, causing it to trigger on every render.
Unlike most JavaScript errors that point to a single failing line, infinite loops are insidious — the code works fine in development when hot-reloading masks the problem, then silently crashes in production. The error message itself is frustratingly vague: "Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside render, or when setState is called during a render (e.g. setState(setState))."
Understanding what triggers it and how to hunt it down is crucial for any React developer shipping to production.
What causes the infinite loop
React checks for infinite update loops because they waste memory and crash the browser tab. When a state update happens, React re-renders the component. If that re-render triggers another state update on the same component, React applies it and re-renders again. This should stop eventually — but if the condition that fires setState is still true after the render, the loop continues.
The most common triggers are:
setState directly in render:
// DON'T do this
function MyComponent() {
const [count, setCount] = useState(0);
setCount(count + 1); // Called during render — fires on every render
return <div>{count}</div>;
}
useEffect with a missing dependency:
// DON'T do this
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Dependency array is empty or missing 'count'
}, []); // This runs every render because no dependency is tracked
return <div>{count}</div>;
}
Calling a function that triggers setState:
function MyComponent() {
const [data, setData] = useState(null);
// handleClick sets state, and if it's called during render, infinite loop
return <button onClick={handleClick}>Load</button>;
}
function handleClick() {
setData({ /* something */ });
}
Conditional logic that re-creates objects:
function MyComponent({ items }) {
const [filter, setFilter] = useState("");
// If filteredItems is a new object on every render (it's not memoized),
// and you pass it as a dependency, the useEffect runs every render
const filteredItems = items.filter(item => item.includes(filter));
useEffect(() => {
setFilter(""); // Runs constantly because filteredItems is new each time
}, [filteredItems]);
return <div>{filter}</div>;
}
How to debug it in production
If you've deployed code that triggers this, the error lands in your error tracker with a stack trace pointing back to your component. Here's how to diagnose the real cause using the debugging workflow:
1. Look at the stack trace. The error should name the component where the loop started. If the trace is minified, make sure you've uploaded source maps so you can read the real file and line numbers.
2. Check the breadcrumbs. Leading up to the crash, what user actions or state changes happened? Breadcrumbs capture clicks, API responses, and console logs — the trail that led to the infinite loop.
3. Isolate the component. Look at the named component's useEffect hooks and any direct setState calls. Is there a missing dependency? Is setState being called unconditionally?
4. Reproduce locally. Use the breadcrumb trail to recreate the exact sequence of user actions. If you can reproduce it, you can attach a debugger and step through the render cycle.
In development, React's StrictMode runs effects twice to catch side effects. This can mask or exaggerate infinite loops. If you see "maximum update depth exceeded" only in production, a missing dependency is a likely culprit.
Fixing the loop
The fix depends on where the loop starts.
If setState is in render, move it to an effect:
function MyComponent() {
const [count, setCount] = useState(0);
// Move the state update into useEffect
useEffect(() => {
setCount(count + 1);
}, []); // Run once on mount, not every render
return <div>{count}</div>;
}
If useEffect is firing too often, fix the dependency array:
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, [count]); // Now it runs only when 'count' changes — but this still loops!
return <div>{count}</div>;
}
// Better: use the state setter callback to avoid dependency on count
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(prev => prev + 1);
}, []); // Safe — runs once on mount
return <div>{count}</div>;
}
If a dependency is a new object every render, memoize it:
import { useMemo } from 'react';
function MyComponent({ items }) {
const [filter, setFilter] = useState("");
// useMemo ensures this object doesn't change unless 'items' or 'filter' changes
const filteredItems = useMemo(
() => items.filter(item => item.includes(filter)),
[items, filter]
);
useEffect(() => {
console.log("Filtered:", filteredItems);
}, [filteredItems]); // Safe — filteredItems only changes when items or filter changes
return <div>{filter}</div>;
}
If you're calling a function that triggers setState, wrap it in useCallback:
import { useCallback } from 'react';
function MyComponent({ onSave }) {
const [data, setData] = useState(null);
const handleSave = useCallback(() => {
setData({ saved: true });
onSave();
}, [onSave]);
// Now handleSave is stable across renders
return <button onClick={handleSave}>Save</button>;
}
Preventing loops in the first place
The best fix is architectural — write components that don't loop in the first place. If you're setting up error tracking for React, make sure your error boundary and SDK are configured so infinite loops get caught.
Use the functional setState form to avoid dependency on the current state:
setCount(prevCount => prevCount + 1); // Safe — doesn't depend on the current value
Keep dependencies tight. Only list the values that actually change — not derived objects or functions. Use useCallback and useMemo to stabilize dependencies.
Use error tracking in development and production. Error tracking will catch infinite loops the moment they land, with a stack trace, affected users, and breadcrumbs showing the path that triggered the loop. That context cuts debugging time from hours to minutes.
Test component lifecycles in isolation. Write tests that verify your useEffect hooks don't fire more times than expected. Libraries like React Testing Library make this straightforward.
If you see "maximum update depth exceeded" only under specific conditions (e.g., when a particular API returns data), the root cause is usually a dependency array that's missing that API response or a state change triggered by that response.
When infinite loops go to production
Even with careful code review, an infinite loop can slip through. It often happens when a hook dependency seems local but actually pulls in a parent prop that changes every render, or when an effect has a side effect that re-triggers the effect itself.
This is where error tracking pays off. When the loop crashes in production, your error tracker shows:
- The exact component and line
- The stack trace (readable if you've uploaded source maps)
- Every user affected and how many times
- The breadcrumb trail leading to the crash
From that, you can pin down whether it's a missing dependency, a logic error, or an edge case you missed. It's part of the broader error tracking best practices that turn errors into actionable insights. Then you deploy the fix with confidence that you've caught the root cause, not just the symptom.
Start tracking errors in minutes
Catch "maximum update depth exceeded" and other React errors in production with stack traces, breadcrumbs, and user context — point the Sentry SDK at LightTrace to start free.
Infinite loops are frustrating because they're often invisible until production. Keep dependencies tight, use the functional setState form, test your hooks, and lean on error tracking to catch the ones that slip through. Your production React app will be more stable for it.