On This Page
You click a button, or your component mounts, and the screen goes white with this in the console:
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.This one is almost always self-inflicted by a single line of JSX, and it is one of the few React errors that tells you exactly what's wrong in the message itself — you just have to know where to look. Let's find it fast, then go under the hood so you stop reintroducing it.
The Error
Development build, thrown synchronously and caught by the nearest error boundary (or React's own root-level handling if you have none):
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
at throwRenderPhaseUpdateLimitError (react-dom-client.development.js:8385:11)
at updateReducerImpl (react-dom-client.development.js:8368:13)
at updateReducer (react-dom-client.development.js:8459:16)
at updateState (react-dom-client.development.js:9012:16)
at Object.useState (react-dom-client.development.js:24895:16)
at Counter (Counter.tsx:6:24)
at renderWithHooksAgain (react-dom-client.development.js:6923:20)
at renderWithHooks (react-dom-client.development.js:6858:22)
at updateFunctionComponent (react-dom-client.development.js:9682:21)
The above error occurred in the <Counter> component:
at Counter (Counter.tsx:6:24)
at div
at App (App.tsx:4:20)
Consider adding an error boundary to your tree to customize error handling behavior.Production build, minified:
Uncaught Error: Minified React error #301; visit https://react.dev/errors/301?args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings.react.dev/errors/301 decodes error #301 straight to the same sentence: Too many re-renders. React limits the number of renders to prevent an infinite loop. React ships minified numeric codes in production purely to save bytes on the wire — the fix is identical either way, but you'll have a much easier time locating the offending component if you reproduce the crash against the development build first (npm run dev, not npm run build && npm run start), since only dev gives you the component stack and the line number inside your file.
This exact message and the #301 code have been stable since Hooks shipped in React 16.8 (February 2019) and are unchanged through the current 19.3 line — see the Version Behavior Matrix below for what has changed around it.
How to Reproduce It (step-by-step)
Scaffold a fresh Vite + React + TypeScript project:
bash
npm create vite@latest repro -- --template react-ts
cd repro
npm installpackage.json dependency block (pinned to what's current as of this writing):
json
{
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@types/react": "19.2.x",
"@types/react-dom": "19.2.x",
"typescript": "5.9.x",
"vite": "7.x"
}
}Replace src/Counter.tsx with the buggy component — the single most common way people hit this, calling the handler instead of passing it:
tsx
// src/Counter.tsx
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={setCount(count + 1)}>
Count: {count}
</button>
);
}Wire it into src/App.tsx:
tsx
// src/App.tsx
import { Counter } from "./Counter";
export default function App() {
return (
<div>
<Counter />
</div>
);
}Run it:
bash
npm run devOpen the page and the error fires immediately, before you even click anything — because setCount(count + 1) is invoked on every render (it's a function call sitting directly in the JSX, not a reference), and each call schedules a state update while React is still rendering Counter. That update triggers another render of Counter, which calls setCount(count + 1) again, and so on. React counts these render-phase update passes and throws once the count passes its limit — you'll get the exact same crash whether the bad call sits in the JSX like this, in a conditional near the top of the function body, or in a derived value computed unconditionally on every render.
To see the production/minified variant, build and serve it instead:
bash
npm run build && npm run previewThe console now shows Minified React error #301 instead of the full sentence, with everything else about the crash identical.
Version Behavior Matrix
This error is version-neutral in the sense that matters most: the message, the #301 code, and the underlying 25-pass limit have not changed across any currently-supported release.
| Release | Behavior |
|---|---|
| React 17 | Same message text, same enforcement, no #301 decoder link (minified errors point to a versioned Facebook GitHub gist instead) |
| React 18.0 – 18.3 | Unchanged. Automatic batching (18.0) and StrictMode effect double-invocation don't change this check — it lives in the render phase, not effects |
| React 19.0 – 19.2 | Unchanged. react.dev/errors/301 is the current decoder link format |
| React 19.3 (current, Sept 2026) | Unchanged |
What is version-relevant is the class-component equivalent, if you're maintaining older code: calling this.setState() unconditionally inside render() doesn't hit the same render-phase-update counter — it produces a different, older warning instead:
Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern but can be moved to `componentWillMount`.That warning predates hooks and is unrelated to the RE_RENDER_LIMIT mechanism described below — it's worth knowing the two are different code paths if you're debugging a mixed class/function codebase.
Why It Happens — Surface Level
Somewhere in the component body, a state setter runs unconditionally during render — not inside an event handler, not inside useEffect, but as a direct side effect of the function component executing. The classic culprits:
onClick={setCount(count + 1)}— calls the setter immediately and passes its return value (undefined) as the handler, instead ofonClick={() => setCount(count + 1)}.if (count !== 0) setCount(0)sitting bare in the component body with no guard that actually converges — every render seescount !== 0is still true (because the update hasn't committed yet on this pass) and callssetCountagain.- Deriving a value with
useState+ an unconditionalsetDerived(...)call in the render body, rather than just computing the value inline or inuseMemo.
Why It Happens — Under the Hood
React's function-component hooks are stored as a linked list hanging off the fiber (fiber.memoizedState), rebuilt in the same call order every render — that's why conditional hooks are fatal for a different reason (position-based lookup breaks), but it's also why this error exists at all: React needs a stable place to detect that you're updating state mid-render.
When you call a setter (dispatchSetStateInternal in ReactFiberHooks.js) while React is actively executing your function component — i.e., during the render phase, before commit — React doesn't schedule an ordinary asynchronous update. It sets a flag (didScheduleRenderPhaseUpdate) and, if the update targets the currently rendering fiber, immediately loops back and re-invokes your component function again synchronously, via a function literally called renderWithHooksAgain. This is a deliberate, supported mechanism — it's what lets you safely call a setter during render to adjust state in response to a prop change (the "adjusting state when a prop changes" pattern from the React docs), without waiting for a whole separate commit-and-rerender cycle.
The safety valve is a counter, numberOfReRenders, incremented on every pass through that loop and checked against a constant:
js
// react-reconciler/src/ReactFiberHooks.js (simplified)
const RE_RENDER_LIMIT = 25;
do {
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
}
numberOfReRenders += 1;
// ... re-run the function component, rebuild the hook list from scratch ...
} while (didScheduleRenderPhaseUpdateDuringThisRender);If your setter call isn't gated by a condition that actually stops being true, React re-renders the component 25 times within the same render phase, before ever reaching commit — the DOM never updates, no effect ever runs, key warnings don't help you — and then throws rather than hanging the tab.
This is a different counter from the one behind Maximum update depth exceeded (Minified React error #185), which is worth naming explicitly since the two get confused constantly: that error comes from throwIfInfiniteUpdateLoopDetected in ReactFiberWorkLoop.js, guarded by NESTED_UPDATE_LIMIT = 50, and it fires when updates are scheduled outside the render phase — typically from an unguarded useEffect that calls its own setter on every commit, so each commit schedules the next render via the normal update queue rather than via the synchronous render-phase loop above. Same family of bug (an update that never converges), two different detection mechanisms, two different error messages, because the two situations are structurally different inside the fiber work loop.
6. The Fix
Cause: calling the handler instead of referencing it.
- <button onClick={setCount(count + 1)}>
+ <button onClick={() => setCount(count + 1)}>
Count: {count}
</button>Cause: an unconditional setState in the render body meant to sync from a prop.
function Panel({ activeId }: { activeId: string }) {
const [selected, setSelected] = useState(activeId);
- if (selected !== activeId) {
- setSelected(activeId);
- }
+ // Adjusting state during render is legitimate React — but it MUST
+ // converge in one pass. Confirm the guard condition can only be
+ // true once per prop change, or better, avoid the pattern entirely:
+ if (selected !== activeId) {
+ setSelected(activeId);
+ return null; // bail this render; the next one sees selected === activeId
+ }
return <div>{selected}</div>;
}That "adjust state during render" pattern is real and documented, but it is easy to get wrong, and the better fix is almost always to not mirror the prop into state at all — see the next section.
Cause: derived value computed via useState + setState in the body instead of during render.
function Cart({ items }: { items: Item[] }) {
- const [total, setTotal] = useState(0);
- setTotal(items.reduce((sum, i) => sum + i.price, 0)); // runs every render, unconditionally
- return <div>Total: {total}</div>;
+ const total = items.reduce((sum, i) => sum + i.price, 0); // just compute it
+ return <div>Total: {total}</div>;
}Best Practices & The Better Design
The fix that actually prevents this class of bug from recurring is to stop storing values in state that can be computed from props or other state during render. React re-renders your component function on every relevant change anyway — a plain const total = … computed inline (or memoized with useMemo if the computation is expensive) can never desync, can never trigger a second render, and can never hit RE_RENDER_LIMIT, because it isn't a state update at all.
tsx
import { useMemo } from "react";
function Cart({ items }: { items: { price: number }[] }) {
const total = useMemo(
() => items.reduce((sum, i) => sum + i.price, 0),
[items],
);
return <div>Total: {total}</div>;
}For the "state should follow a prop" case specifically, prefer resetting via key over mirroring the prop into local state with an effect or a render-phase setState:
tsx
// Parent remounts Panel with fresh state whenever activeId changes —
// no synchronization code needed, and nothing can loop.
<Panel key={activeId} activeId={activeId} />Where you have several pieces of state that must change together in response to one event, replace the tangle of useState + guarded setState calls with useReducer, so the transition is one atomic dispatch instead of several setters racing across renders.
Prevent It in the Long-Term
eslint-plugin-react-hooks(v7, flat config) —react-hooks/rules-of-hooksandreact-hooks/exhaustive-depscatch the hook-order and stale-dependency half of this bug family; the newer React-Compiler-powered rules go further and catch the render-phase-update pattern itself:react-hooks/set-state-in-renderflags a setter called unconditionally in the component body, andreact-hooks/purityflags side effects (including state writes) that make a component's render impure. Enable them viareactHooks.configs.flat.recommendedineslint.config.js.- React Compiler in CI — the compiler refuses to safely memoize components that violate the Rules of React (including unguarded render-phase state writes), so a compiler build failure in CI is an early, precise signal for exactly this bug — often before anyone opens the app.
react/no-unstable-nested-components(fromeslint-plugin-react) — components defined inside another component's render body commonly grow their own runaway state logic; flagging the nested definition removes a whole category of these bugs by construction.- Playwright/Cypress console-error gating — fail the CI build if a Playwright or Cypress run logs an uncaught
Erroror a React warning to the console; this catches the crash in an E2E smoke test rather than in a user's browser. - React DevTools Profiler — a component that renders 25 times before commit shows up as an anomalous, instant flamegraph spike; use
<Profiler onRender>in development to alarm on render counts that spike well past what a normal interaction should cause. - Sentry (or your error monitor) on the error boundary — wire
onCaughtError(React 19) or your boundary'scomponentDidCatchto report#301crashes with the component stack attached, so a regression that only reproduces on one code path in production still surfaces quickly.
Important
Too many re-renders/ minified#301means a state setter is being called unconditionally during the render phase, and it will always keep re-triggering itself — checkRE_RENDER_LIMIT(25) inReactFiberHooks.jsif you want to see the exact mechanism.- The single most common cause is
onClick={fn()}instead ofonClick={fn}— always double-check you're passing a reference, not invoking the function inline in JSX. - This is a different bug and a different internal counter from
Maximum update depth exceeded(#185,NESTED_UPDATE_LIMIT = 50) — that one comes from an unguardedsetStateinsideuseEffect, scheduled across separate commits rather than within one render pass. - The durable fix is almost always to stop mirroring derivable values into state at all — compute them inline or with
useMemo, or reset withkeyinstead of syncing a prop into state. eslint-plugin-react-hooks's newerreact-hooks/set-state-in-renderandreact-hooks/purityrules, plus the React Compiler, catch this class of bug at lint/build time rather than at runtime.
CODELZ Newsletter
Join the newsletter to receive the latest updates in your inbox.