The Emergency Brake
React just pulled the emergency brake. This error usually means your component is caught in a loop, re-rendering so fast it would freeze the browser if React didn't stop it. You'll see a wall of red text in your console, and the UI will likely stop responding.
Error: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect...
TL;DR Quick Fixes
- Missing the Array: If the effect should only run once when the component mounts, add
[]as the second argument. - State Feedback Loops: Updating state that is also a dependency? Switch to a functional update:
setCount(c => c + 1)instead ofsetCount(count + 1). - Unstable References: If your dependency is an object or array defined inside your component, it's a "new" object every render. Wrap it in
useMemo. - The Guard Clause: Wrap your
setStatein anifstatement to ensure it only fires when the data actually changes.
Why This Happens
Think of this error as a safety net. React monitors your render cycles and intervenes if it detects more than 50 consecutive updates in a single second. In useEffect, this is almost always a circular chain reaction: Render β Effect β State Change β Re-render β Effect.
1. The Missing Dependency Array
Forgetting the second argument is a common slip-up. Without it, the effect runs after every single render. If that effect updates state, it forces a new render, which runs the effect again. Itβs a never-ending cycle.
// β BAD: Infinite loop
useEffect(() => {
setUserData(data);
}); // No array here!
2. The Referential Equality Trap
JavaScript compares objects and arrays by reference, not by value. In plain English: [] === [] is false. If you define a constant object inside your component and pass it to a dependency array, React thinks it's a brand new dependency every time the component renders.
// β BAD: 'options' is a new memory reference every render
const options = { theme: 'dark' };
useEffect(() => {
// Logic here
}, [options]);
How to Fix It
Approach 1: Use Functional Updates
Stop passing the current state into the dependency array just to update it. By using a functional update, you can remove the state variable from the dependency list entirely, breaking the loop.
// β BAD: 'count' in the array causes a loop
useEffect(() => {
const timer = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(timer);
}, [count]);
// β
GOOD: No 'count' dependency needed
useEffect(() => {
const timer = setInterval(() => setCount(prev => prev + 1), 1000);
return () => clearInterval(timer);
}, []);
Approach 2: Stabilize Objects with useMemo
If an object must be a dependency, ensure its reference stays the same across renders. useMemo caches the object so React only sees it as "changed" when its internal values actually shift.
// β
GOOD: 'options' only changes if 'theme' changes
const options = useMemo(() => ({
color: theme === 'dark' ? '#fff' : '#000'
}), [theme]);
useEffect(() => {
// Logic runs only when the specific theme changes
}, [options]);
Approach 3: The Manual Comparison
Before you trigger a state update, check if you actually need to. This simple guard clause prevents unnecessary re-renders from snowballing into a crash.
useEffect(() => {
if (newId !== currentId) {
setCurrentId(newId);
}
}, [newId, currentId]);
How to Verify the Fix
- Watch the Console: Refresh the page. The red error block should vanish.
- Check the Network Tab: If your effect fetches data, ensure you don't see dozens of identical requests firing per second. One or two is normal; fifty is a bug.
- React Profiler: Use the Profiler in React DevTools. If the "Commit" timeline shows a flat line of rapid updates for one component, the loop is still there.
Further Reading
- React Documentation: Synchronizing with Effects
- React Documentation: Functional Updates
- MDN Web Docs: Equality Comparisons in JS

