How to fix the React useEffect infinite loop (and 'Too many re-renders')
The #1 React question on Stack Overflow, solved properly: why useEffect loops forever, what 'Too many re-renders' actually means, and the dependency-array patterns that fix both for good.
If you've ever watched your network tab fire the same request hundreds of times a second, or hit React's 'Too many re-renders. React limits the number of renders to prevent an infinite loop' error, you've met the most-asked React question on Stack Overflow. Both symptoms have the same root cause: something in your render cycle triggers a state update, which triggers a render, which triggers the same thing again.
Cause 1: setting state inside useEffect with no dependency array
// โ Runs after EVERY render, and setCount causes a render
useEffect(() => {
setCount(count + 1);
});
// โ
Runs once on mount
useEffect(() => {
setCount((c) => c + 1);
}, []);Without a dependency array, the effect runs after every render. If the effect sets state, you've built a loop by hand. Add the array - empty for mount-only work, or listing exactly the values the effect reads.
Cause 2: objects, arrays, or functions in the dependency array
This is the sneaky one, because the dependency array looks correct. React compares dependencies by reference, and an object or array literal created during render is a brand-new reference every single time. So `[filters]` where `filters = { status: 'open' }` is defined in the component body will re-run the effect on every render, forever.
// โ New object every render โ effect fires every render
const options = { page, pageSize: 20 };
useEffect(() => { fetchUsers(options); }, [options]);
// โ
Depend on the primitives instead
useEffect(() => {
fetchUsers({ page, pageSize: 20 });
}, [page]);The fix hierarchy: depend on primitives when you can, move the object inside the effect when you can't, and reach for useMemo/useCallback only when the value genuinely has to live outside the effect. Memoizing everything by reflex just hides the design problem.
'Too many re-renders' is usually not useEffect at all
// โ Calls the handler during render
<button onClick={handleClick()}>Save</button>
// โ Sets state directly in the component body
if (isOpen) setCount(count + 1);
// โ
Pass a reference, don't invoke
<button onClick={handleClick}>Save</button>That error means state was set synchronously during render - most often `onClick={fn()}` instead of `onClick={fn}`, or a bare setState in the component body. React bails out after ~50 nested updates and throws. Move the update into an event handler or an effect with correct dependencies.
Debugging checklist
- Turn on the eslint-plugin-react-hooks exhaustive-deps rule - it catches 90% of these at lint time.
- console.log a render counter; if it climbs without user interaction, binary-search your effects by commenting them out.
- For each looping effect, ask: which dependency changed? React DevTools' 'why did this render' highlight answers it quickly.
- Use the functional update form setState(prev => ...) so the effect doesn't need the state value as a dependency at all.
An infinite loop is never random - one of your dependencies changes identity on every render. Find that dependency and you've found the bug.
Written by Appesto Engineering.