What you'll learn
Quick Answer
The errorToo many re-renders. React limits the number of renders to prevent an infinite loop.means your component called a state setter such assetCountdirectly during rendering. That schedules another render, which calls the setter again, with no exit. State updates belong in event handlers or effects, not in the function body. The usual culprits are an unconditional setter call in the body, or writingonClick={handleClick()}when you meantonClick={handleClick}.
What the Error Actually Says
The full message is Too many re-renders. React limits the number of renders to prevent an infinite loop. It is thrown during rendering, not from a click handler or a network callback.
Rendering is React calling your component function to work out what the UI should look like. That call is supposed to be pure: same props and state in, same JSX out, no side effects. Calling a state setter is a side effect, and it schedules another render. If your function does that every time it runs, render 1 triggers render 2 triggers render 3, and nothing ever breaks the chain.
React counts consecutive re-renders that happen without a commit to the screen in between. Once that count hits 25 it stops and throws this error instead of letting the tab freeze. The exact number is an internal constant, not something you configure, and it is not the thing to focus on. Any state update that runs unconditionally in the render path will hit it on the first try.
Cause 1: Calling the Setter in the Body
The clearest version of the bug:
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // runs on every render
return <p>{count}</p>;
}Trace it: the component mounts, the function runs, setCount(1) is queued, React re-renders, setCount(2) is queued, and so on. After 25 rounds React aborts with Too many re-renders. Verified: this throws that exact message immediately.
Written out this plainly it looks obviously wrong, but in real code it hides. It shows up as "initialise this piece of state from a prop," as transforming a prop into a formatted string and calling setFormatted(...) in the body, or as calling a context updater during render to register the component. The pattern is the same each time: a setter that runs unconditionally on the render path.
The fix is to decide when the update should happen. Once, on mount? useEffect(() => { setCount(1); }, []). In response to a click or a fetch resolving? Put it in that callback. The honest answer is often "never" - the value is derived from something else and should be calculated during render, not stored, which the last section covers.
Cause 2: Calling a Handler Instead of Passing It
<button onClick={handleClick()}> // WRONG - runs during render
<button onClick={handleClick}> // right - passes the function
<button onClick={() => handleClick(id)}> // right - when you need an argumentThe parentheses in handleClick() run the function while React is building the element, and whatever it returns becomes the click handler. If handleClick calls setState, you get the loop - verified, it throws the same Too many re-renders message. If it does not call setState, you get a quieter bug: the "handler" is now the function's return value, usually undefined, and clicking does nothing.
The JSX prop wants a function it can call later, when the click actually happens. So hand it a bare name, or wrap the call in an arrow. Any time you need to pass an argument, () => fn(arg) is the pattern; fn(arg) on its own runs immediately. The same trap appears with onChange, onSubmit, and any prop that expects a callback - and with custom components, where onDone={finish()} looks just as reasonable and fails the same way. Verified: onClick={handleClick} and the arrow form both render fine with no loop.
The Look-Alike: A useEffect That Loops
useEffect(() => {
setCount(count + 1); // no dependency array
});This also re-renders forever, but it does not throw Too many re-renders. Verified: it ran past 30 renders with no exception; in a real browser the tab simply locks up. The distinction matters for diagnosis.
Too many re-renders means a setter ran in the render phase - React catches it synchronously and the stack trace points near the offending line. A silent freeze, or a re-render count climbing in the React DevTools profiler, points instead at an effect that re-runs on every render. Common versions: no dependency array at all; an object or array literal in the deps ([{ id }] is a fresh object each render, so it never looks equal); a function from props that is not wrapped in useCallback; or listing a value in the deps that the effect itself updates.
Fix it by making the dependency list accurate and stable, and by guarding the state update with a condition that eventually becomes false.
The Three Fixes
1. Derive, do not store. If a value can be computed from props or existing state, compute it during render:
function Cart({ items }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
return <p>Total: {total}</p>;
}Verified: this renders Total: 350 for two items priced 100 and 250 - no state, no effect, no loop. Putting total in state and syncing it with an effect is the bug waiting to happen.
2. Move the update to the event. A click, a submit, a response arriving - all belong in handlers or effects, never the function body.
3. A conditional update that converges is allowed. The documented "adjust state when a prop changed" pattern is legal:
const [prevId, setPrevId] = useState(id);
if (id !== prevId) {
setPrevId(id);
setSelected(null);
}Verified: on a prop change React re-runs the component right away, the condition is now false, and it settles - three renders total, no error. The setter in the body is fine here precisely because it cannot fire on two consecutive renders.
