How to Fix React Infinite Re-Render Loop
On this page
Few bugs are as frustrating as the React infinite re-render loop. One moment your component renders fine, and the next your browser is frozen, your console is flooded with the dreaded "Too many re-renders. React limits the number of renders to prevent an infinite loop" error, and your fans are spinning up. The good news is that infinite render loops almost always come from a small set of recognizable mistakes. Once you learn to spot the patterns, fixing them becomes routine.
This guide walks through why infinite re-renders happen, the most common causes, and concrete fixes you can apply today.
What Actually Causes an Infinite Re-Render
React re-renders a component whenever its state or props change. An infinite loop happens when rendering itself triggers a state update, which causes another render, which triggers another update, and so on forever.
The core rule to internalize is this: never update state directly during render. State updates belong in event handlers and effects, not in the render body.
Here is the classic mistake:
function Counter() {
const [count, setCount] = useState(0);
// ❌ Calls setCount during render → infinite loop
setCount(count + 1);
return <p>{count}</p>;
}
Every render calls setCount, which schedules another render, which calls setCount again. React detects the runaway and throws the "Too many re-renders" error.
Cause #1: Calling a Function Instead of Passing a Reference
This is the single most common beginner trap. When you wire up an event handler, you must pass a reference to the function, not call it.
// ❌ Calls handleClick during render, which sets state immediately
<button onClick={handleClick()}>Click me</button>
// ✅ Passes a reference; React calls it only on click
<button onClick={handleClick}>Click me</button>
If handleClick updates state, the broken version runs it on every render, creating a loop. If your handler needs arguments, wrap it in an arrow function:
// ✅ Arrow function defers the call until the click happens
<button onClick={() => handleClick(id)}>Click me</button>
Cause #2: Updating State Directly in the Component Body
Sometimes you genuinely need to derive or synchronize a value. The temptation is to call setState inline, but that loops. Instead, derive the value during render without storing it in state at all.
// ❌ Storing derived data in state and setting it during render
function Cart({ items }) {
const [total, setTotal] = useState(0);
setTotal(items.reduce((sum, i) => sum + i.price, 0)); // loop!
return <p>{total}</p>;
}
// ✅ Just compute it — no state needed
function Cart({ items }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
return <p>{total}</p>;
}
The best fix for many "infinite loop" bugs is realizing the value should never have been state in the first place. If you can calculate something from existing props or state, compute it during render.
Cause #3: useEffect with Missing or Wrong Dependencies
useEffect is the second-biggest source of loops. The pattern is: an effect updates state, and that state (or an object it depends on) is in the dependency array, so the effect runs again.
// ❌ Effect sets state that it depends on
useEffect(() => {
setData([...data, newItem]);
}, [data]); // data changes → effect runs → data changes → ...
Fixes depend on intent:
Use the functional updater so you don't need the value as a dependency:
useEffect(() => {
setData(prev => [...prev, newItem]);
}, [newItem]);
Run the effect only once by using an empty dependency array when it should mount-only:
useEffect(() => {
fetchInitialData().then(setData);
}, []); // runs a single time after mount
Be honest about dependencies. Don't silence the lint rule blindly — instead restructure the effect so the dependencies are stable.
Cause #4: Objects, Arrays, and Functions as Dependencies
This one is sneaky. In JavaScript, {} !== {} and [] !== []. Every render creates a brand-new object or array reference, so if one ends up in a dependency array, the effect thinks it changed every single render.
// ❌ options is a new object on every render
function Search({ query }) {
const options = { query, limit: 10 };
useEffect(() => {
fetchResults(options).then(setResults);
}, [options]); // new reference each render → loop
}
Fix it with useMemo to stabilize the reference, or depend on the primitive values directly:
// ✅ Memoize the object so its reference is stable
const options = useMemo(() => ({ query, limit: 10 }), [query]);
useEffect(() => {
fetchResults(options).then(setResults);
}, [options]);
The same applies to functions passed as dependencies — wrap them in useCallback:
const handleFetch = useCallback(() => {
fetchResults(query).then(setResults);
}, [query]);
useEffect(() => {
handleFetch();
}, [handleFetch]);
Cause #5: Passing New References to Child Components
A child wrapped in React.memo will still re-render if you pass it a freshly-created object, array, or inline function as a prop. If that child also lifts state up, you can create a loop across components.
// ❌ New array and new function every render
<List items={[1, 2, 3]} onSelect={(x) => select(x)} />
// ✅ Stabilize both
const items = useMemo(() => [1, 2, 3], []);
const onSelect = useCallback((x) => select(x), []);
<List items={items} onSelect={onSelect} />
A Practical Debugging Workflow
When you hit a loop, follow these steps in order:
- Read the stack trace. React usually points to the component and line where the offending
setStatelives. - Search for state setters in the render body. Any
setX(...)not inside an event handler, effect, or callback is suspect. - Check every
onClick,onChange, etc. Look for accidental function calls (onClick={fn()}) instead of references. - Audit your
useEffectdependency arrays. Ask: does this effect set state that appears in its own deps? Are any deps freshly-created objects/arrays/functions? - Add logging. A simple
console.log('render', count)at the top of the component reveals how fast it's spinning and which value keeps changing. - Use the React DevTools Profiler to see which component re-renders and why.
Prevention Tips
- Prefer derived values over state. If you can compute it, don't store it.
- Use functional state updates (
setX(prev => ...)) to avoid depending on current state inside effects. - Keep effects narrow. An effect should do one thing, with a minimal, honest dependency array.
- Enable the
eslint-plugin-react-hooksrules. The exhaustive-deps warning catches most dependency mistakes before they ship. - Memoize objects and callbacks that flow into dependency arrays or memoized children.
FAQ
Why do I get "Too many re-renders" only sometimes?
The error fires when React exceeds its render limit in a single pass — usually from setting state directly during render. If your loop is driven by an effect instead, you may not see that exact error; instead the component re-renders rapidly and the browser hangs. Both are infinite loops, just triggered differently.
Is it ever okay to call setState during render?
Rarely. React does support a "derive state during render" pattern where you call setState conditionally during render to adjust to a prop change — but it must be guarded by a condition that eventually becomes false, otherwise it loops. In almost all cases, computing the value directly or using an effect is clearer and safer.
Will useMemo and useCallback fix every loop?
No. They fix loops caused by unstable references in dependency arrays. They won't help if the real problem is setting state directly in render or calling a handler instead of passing it. Diagnose the cause first, then reach for the right tool.
How do I know which dependency is causing the loop?
Temporarily log each dependency at the top of your effect, or compare references across renders using a useRef. If a value logs as "changed" every render even though it looks the same, it's an unstable object/array/function reference — memoize it.
Does this happen with the useReducer hook too?
Yes. Dispatching an action during render, or in an effect whose dependencies include the resulting state, produces the same loop. The fixes are identical: dispatch from event handlers or carefully-scoped effects.
Conclusion
Infinite re-render loops feel mysterious until you internalize one principle: rendering must be pure — it should never trigger its own state updates. Keep state setters out of the render body, pass function references rather than calling them, write honest useEffect dependency arrays, and stabilize object and function references with useMemo and useCallback. Master those habits and the "Too many re-renders" error becomes a rare, quickly-solved visitor rather than a recurring nightmare.