Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. React is telling you that rendering caused a state update, which caused a render, which caused an update. The cause is almost always one of four patterns, and each has a clear fix.
๐ Table of Contents
- What Is Actually Happening
- Cause 1: Calling the Handler Instead of Passing It
- Cause 2: useEffect Without a Dependency Array
- Cause 3: A Dependency That Changes Every Render
- Cause 4: Setting State to Derive a Value
- The Conditional-Render Variant
- Finding the Culprit
- Quick Reference
- Frequently Asked Questions
- Conclusion
What Is Actually Happening
Rendering must be a pure calculation of UI from props and state. If it also sets state, you create a cycle. React detects the runaway and stops it rather than letting the tab freeze.
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // runs during render -> triggers render -> runs again
return <div>{count}</div>;
}
The error message is unusually accurate here: something in your render path is updating state every time it runs.
Cause 1: Calling the Handler Instead of Passing It
By far the most common version, and the one that catches everyone at least once.
function App() {
const [count, setCount] = useState(0);
// โ setCount(count + 1) is CALLED during render.
// Its return value (undefined) becomes the onClick handler.
return <button onClick={setCount(count + 1)}>Increment</button>;
}
onClick needs a function to call later. Writing setCount(...) executes it immediately. Wrap it in an arrow function so a function is passed rather than a result.
// โ
A function is passed; it runs on click.
<button onClick={() => setCount(count + 1)}>Increment</button>
// โ
Or reference an existing function without calling it.
<button onClick={handleClick}>Increment</button> // no parentheses
// โ This calls handleClick during render.
<button onClick={handleClick()}>Increment</button>
The rule: parentheses after a handler name in JSX means it runs during render. If you need to pass arguments, wrap it.
<button onClick={() => handleDelete(item.id)}>Delete</button>
Cause 2: useEffect Without a Dependency Array
An effect with no second argument runs after every render. If it sets state, that state change triggers another render, which runs the effect again.
function Profile({ userId }) {
const [user, setUser] = useState(null);
// โ No dependency array โ runs after every render, forever.
useEffect(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
});
}
// โ
Re-runs only when userId changes.
useEffect(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]);
Omitting the array is different from passing an empty one. No array means “every render”; [] means “once on mount”.
Cause 3: A Dependency That Changes Every Render
Objects, arrays, and functions are compared by reference. A new one each render means the dependency always looks changed.
function Search({ query }) {
const [results, setResults] = useState([]);
// New object identity on every render.
const options = { query, limit: 20 };
useEffect(() => {
search(options).then(setResults);
}, [options]); // โ always "changed" -> loop
}
Depend on primitives, which compare by value.
useEffect(() => {
search({ query, limit: 20 }).then(setResults);
}, [query]); // โ
string compares by value
When the object must exist outside the effect, memoise it.
const options = useMemo(() => ({ query, limit: 20 }), [query]);
useEffect(() => {
search(options).then(setResults);
}, [options]); // โ
identity is stable until query changes
Cause 4: Setting State to Derive a Value
Using state plus an effect to compute something from existing values creates an unnecessary cycle and an extra render.
function Cart({ items }) {
const [total, setTotal] = useState(0);
// โ Unnecessary, and loops if the dependency is wrong.
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
});
return <p>Total: {total}</p>;
}
Anything computable from props or state should be computed during render.
function Cart({ items }) {
// โ
No state, no effect, no possible loop.
const total = items.reduce((sum, i) => sum + i.price, 0);
return <p>Total: {total}</p>;
}
If the calculation is genuinely expensive, wrap it in useMemo โ but still do not put it in state.
const total = useMemo(
() => items.reduce((sum, i) => sum + i.price, 0),
[items]
);
The Conditional-Render Variant
Setting state inside a conditional during render is the same bug with a disguise.
function Form({ initialValue }) {
const [value, setValue] = useState('');
// โ Still a state update during render.
if (initialValue && !value) {
setValue(initialValue);
}
}
Initialise state properly instead. If the initial value is expensive to compute, pass a function so it only runs once.
const [value, setValue] = useState(initialValue ?? '');
// Lazy initialiser โ the function runs on the first render only.
const [rows, setRows] = useState(() => parseLargeCsv(raw));
If the value must reset when a prop changes, the idiomatic solution is a key on the component, which remounts it with fresh state.
<Form key={userId} initialValue={user.name} />
Finding the Culprit
When the cause is not obvious, log where renders originate.
function MyComponent(props) {
console.count('MyComponent render');
console.log('props:', props);
// ...
}
React DevTools Profiler with “Record why each component rendered” enabled tells you which prop or state changed on each pass. For effect loops, log inside the effect and inside the setter to see the cycle directly.
useEffect(() => {
console.log('effect ran, deps:', { userId, options });
}, [userId, options]);
Quick Reference
| Symptom | Likely cause | Fix |
|---|---|---|
| Errors instantly on mount | Handler called during render | Wrap in an arrow function |
| Loops after data loads | Effect missing a dependency array | Add [] or the correct deps |
| Loops despite correct deps | Object or function dependency | Use primitives, useMemo, or useCallback |
| Extra render on every change | Derived value stored in state | Compute during render |
| Loops on a conditional | setState inside render branch | Initialise state or use key |
Frequently Asked Questions
Q: Why does this only happen sometimes?
A: Loops that depend on data need the data to arrive first. The bug is present from the start but only triggers once a fetch resolves and sets state.
Q: Is it ever acceptable to set state during render?
A: React permits one narrow case โ adjusting state during render when a prop changes, updating the same component only, and guarded by a comparison. It is rarely the best answer; prefer computing during render or using key.
Q: Does this happen in class components?
A: Yes. Calling setState in render or unconditionally in componentDidUpdate produces the identical loop.
Q: My effect has an empty array but still loops. Why?
A: Then the loop is elsewhere โ most often a handler invoked during render, or a parent re-mounting the child because its key changes every render.
Q: Will the React Compiler fix this for me?
A: It removes the memoisation category of the problem by handling identity automatically. It does not fix calling a handler during render, which is a straightforward mistake in the JSX.
Conclusion
“Too many re-renders” always means state is being updated as part of rendering. Check the four causes in order: a handler called instead of passed in JSX, a useEffect with no dependency array, an object or function dependency that changes identity every render, and derived values stored in state instead of computed. The underlying rule is simple โ rendering must be a pure calculation, and every state update must originate from an event handler or an effect, never from the render itself.
๐ You might also like
๐ Share this article




โ๏ธ Leave a Comment