1. Calling setState directly inside the render body
I know this error is infuriating—you're staring at a blank screen and React's just telling you it gave up. This is the #1 cause, and I've seen it trip up devs from juniors to seniors. The pattern looks innocent:
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // BUG: runs on every render
return <div>{count}</div>;
}
What happens? React renders the component, hits setCount, which schedules a state update. That triggers a re-render. On the re-render, it hits setCount again. Infinite loop. React catches it after about 50 re-renders and throws the error.
The fix is simple: never call a state setter directly in the render body. You should only call it inside event handlers, useEffect, or other lifecycle hooks. If you need to initialize state from props, use the useState initializer function instead:
function Counter({ initialCount }) {
const [count, setCount] = useState(() => initialCount);
// setCount is only called by user actions or effects, not here
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
If you're doing this by accident, you'll often see it inside a useMemo or useCallback callback that's called during render. Check any function that's not an event handler—if it calls setState directly, that's your culprit.
2. Missing or incorrect useEffect dependencies
This one's sneakier. You've got a useEffect that updates state, and you forgot to add that state variable to its dependency array. Or you added it, but the effect triggers on mount and updates the state, which re-runs the effect because the state changed. Classic loop.
Here's a real-world trigger: a component that fetches data and updates a local state based on a prop change.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // BUG: userId missing from deps
// Then somewhere else:
useEffect(() => {
if (user) {
setUser({ ...user, loaded: true });
}
}, [user]); // This runs, sets state, triggers re-render...
return <div>{user?.name}</div>;
}
The first effect runs once on mount (empty deps). When userId changes, it doesn't re-run—so user stays stale. But the second effect depends on user, so when the fetch completes and setUser runs, it triggers the second effect, which sets state again, and if that creates a new object reference, you get a loop.
Fix: include all relevant dependencies and avoid setting state in effects that depend on that same state unless you're sure the update won't create a new reference every time. Use a functional update or a ref to track changes.
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Now it re-fetches when userId changes
If your effect sets state based on its own previous value, use the functional form: setCount(prev => prev + 1). That avoids the loop entirely because React batches the update and doesn't re-run the effect until the next render cycle.
3. Event handlers called during render instead of being passed as references
This one's a classic JSX gotcha. You write onClick={handleClick()} instead of onClick={handleClick}. The parentheses call the function immediately during render, and if that function calls setState, you're in a loop.
I see this most often with inline arrow functions or when someone tries to pass arguments:
function ButtonList() {
const [activeId, setActiveId] = useState(null);
return (
<div>
{items.map(item => (
<button onClick={setActiveId(item.id)}> // BUG: calls setState during render
{item.name}
</button>
))}
</div>
);
}
Every render, setActiveId(item.id) runs immediately for each button, which triggers a state update, which triggers another render, and around we go.
The fix: wrap the call in an arrow function or use a curried handler:
<button onClick={() => setActiveId(item.id)}>{item.name}</button>
Or better, create a separate handler:
const handleClick = (id) => () => setActiveId(id);
// ...
<button onClick={handleClick(item.id)}>{item.name}</button>
Same rule applies to onSubmit, onChange, onMouseEnter, etc. If you see a function call with parentheses in JSX, ask yourself: should this run now, or later when the event fires?
Quick-reference summary
| Cause | What to check | The fix |
|---|---|---|
| setState in render body | Any setter call outside event handlers, useEffect, or useMemo | Move it inside an event handler or useEffect |
| Bad useEffect dependencies | Missing deps or state-updating effect that depends on that state | Add missing deps; use functional update to avoid reference changes |
| Event handler called during render | Parentheses in JSX event props like onClick={fn()} |
Pass reference: onClick={fn} or wrap in arrow: onClick={() => fn()} |
One more thing: if none of these work, check for useEffect or useLayoutEffect that updates state based on a prop or context value that itself changes on every render (like a new object created inline). That's a variant of cause #2, but the fix is often memoizing the value with useMemo or useCallback.
You've got this. The error message is React's safety net—it's protecting you from crashing the browser tab. Once you know the three patterns, you'll spot them in seconds.