Your component crashed on its first render and the console shows:
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.React 19 follows it with An error occurred in the <TodoList> component. (React 18 printed The above error occurred in the <TodoList> component: plus a component stack). Production builds show Minified React error #301 instead. It means a state setter was called while the component itself was rendering, unconditionally, so each render asked for another one. React stops after 25 consecutive re-renders of the same component and throws rather than freezing the tab.
Quick fix checklist
- Look for event props that call a setter instead of passing a function:
onClick={setOpen(true)}should beonClick={() => setOpen(true)}. - Same for your own handlers:
onClick={handleSave()}runshandleSaveduring render; writeonClick={handleSave}. - Search the component body (outside handlers and effects) for any
setSomething(...)call that is not inside anifthat eventually becomes false. - If you are copying props into state and “syncing” them in the body, delete the state and compute the value as a variable.
- If you really must adjust state when a prop changes, guard it by comparing with the previous prop stored in state.
- Use the component name from the error (
<TodoList>) to know which file to open.
Before you start
You should know that a function component is a plain function React calls to get JSX, and that calling a useState setter schedules a new render rather than changing the variable you already have. If that second part is fuzzy, read React state snapshots first. Everything below applies to React 18 and 19; only the surrounding console text differs.
Why it happens
Rendering must be pure: React calls your component, reads the JSX it returns and decides what to change in the DOM. Setting state is a request to render again. Those two facts collide when a setter runs as part of rendering:
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // runs on every render
return <p>{count}</p>;
}React supports one narrow form of render-time update: if a component sets its own state while rendering, React throws away the JSX it just got and immediately calls the component again with the new state, before touching the DOM. That is useful for the guarded “adjust state when a prop changes” pattern described later. But if the setter runs on every call, the component never produces a stable result. React counts these immediate re-renders and throws after 25.
One detail surprises people: during render, React does not skip the re-render when the new value equals the old one. setX(1) when x is already 1 still triggers another pass, so this loops forever too:
function Toggle() {
const [open, setOpen] = useState(false);
return <button onClick={setOpen(true)}>{String(open)}</button>;
}The braces in onClick={setOpen(true)} evaluate an expression while building the JSX. That expression is a function call, so setOpen(true) runs during render and its return value (undefined) becomes the click handler. The same happens with onClick={handleClick()} when handleClick sets state, and with onChange={setName(e.target.value)} copied from a tutorial.
The third common source is deriving state in the body: keeping a filtered list, a total or a formatted string in useState and “updating” it at the top of the component each render. The setter runs every time, so it loops.
Step-by-step walkthrough
Step 1: Find the component
The message is generic, but React names the component: An error occurred in the <TodoList> component in React 19, or the top of the component stack in React 18. The topmost frame from your own code in the stack trace is usually the line calling the setter. If you only see Minified React error #301, reproduce it in the development build to get names and source maps.
Step 2: List every setter call that runs during render
Split the component into three zones: the body (runs during render), event handlers (run on user actions) and effects (run after commit). A setter is only a problem in the body. Watch for two shapes:
// Direct call in the body
setTotal(items.reduce((sum, item) => sum + item.price, 0));
// Hidden call inside a JSX attribute: also the body
<button onClick={setPage(page + 1)}>Next</button>
<input onChange={handleChange()} />Anything in { } inside JSX is evaluated while rendering. If it contains (...) directly after a function name, that function runs now.
Step 3: Pass functions to event props
Event props expect a function React will call later:
<button onClick={() => setPage(page + 1)}>Next</button>
<button onClick={handleSave}>Save</button>
<button onClick={() => handleDelete(item.id)}>Delete</button>The arrow is created during render but its body only runs on click. Pass a reference when no arguments are needed; wrap in an arrow when they are.
Step 4: Replace synced state with a calculation
If the setter in the body exists to keep one piece of state in step with props or other state, the state is redundant. Compute it:
// Before: state that mirrors other data
const [total, setTotal] = useState(0);
setTotal(items.reduce((sum, item) => sum + item.price, 0));
// After: a plain variable recomputed each render
const total = items.reduce((sum, item) => sum + item.price, 0);This fixes most cases. If the calculation is genuinely expensive, wrap it in useMemo rather than moving it into state. Derived state in React covers this decision in more depth.
Step 5: Guard a render-time update when you really need one
Occasionally you need state that resets when a prop changes, such as an editable draft that should reset when you switch to another user. The supported pattern stores the previous prop and only sets state when it differs, so the second pass makes the condition false:
function ProfileForm({ user }) {
const [draft, setDraft] = useState(user.name);
const [prevUserId, setPrevUserId] = useState(user.id);
if (user.id !== prevUserId) {
setPrevUserId(user.id);
setDraft(user.name);
}
return <input value={draft} onChange={(e) => setDraft(e.target.value)} />;
}Often an even simpler option exists: render <ProfileForm key={user.id} user={user} /> and React gives each user a fresh component with fresh state, no comparison needed.
Interview tip
Recent versions of
eslint-plugin-react-hooksinclude aset-state-in-renderrule that flags unconditional setters in the component body. Turn on the recommended config so this is caught in the editor.
Worked scenario
A task list with filter buttons crashes as soon as the page loads:
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
const [visible, setVisible] = useState(todos);
setVisible(todos.filter((t) => filter === 'all' || t.done === (filter === 'done')));
return (
<>
<button onClick={setFilter('all')}>All</button>
<button onClick={setFilter('done')}>Done</button>
<ul>
{visible.map((t) => <li key={t.id}>{t.text}</li>)}
</ul>
</>
);
}Diagnosis: the error names <TodoList>. Walking the render zone finds three setter calls that run during render: setVisible(...) in the body (a new array every time) and both setFilter(...) calls in the onClick attributes. Any one of them alone is enough to trigger the error. visible is derived from todos and filter, so it should not be state at all.
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
const visible = todos.filter((t) => filter === 'all' || t.done === (filter === 'done'));
return (
<>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('done')}>Done</button>
<ul>
{visible.map((t) => <li key={t.id}>{t.text}</li>)}
</ul>
</>
);
}Now there is one piece of state (what the user chose) and everything else is computed from it, so a new todos array from the parent is reflected on the same render.
Common mistake
The tempting fix is to move the setter into a useEffect:
useEffect(() => {
setVisible(todos.filter((t) => filter === 'all' || t.done === (filter === 'done')));
});The crash goes away, but you have swapped one loop for another. With no dependency array the effect runs after every render, sets a new array, causes a render, and runs again; React then warns Maximum update depth exceeded (see Maximum update depth exceeded). Even with [todos, filter] as dependencies it renders twice per change and shows stale data for one frame. An effect is for synchronising with something outside React, not for computing values from props.
Another wrong fix is a boolean flag like if (!initialised) { setVisible(...); setInitialised(true); }. It stops the loop but freezes visible at its first value, so later changes to todos are ignored.
Verify the behavior
With React Testing Library you can prove both the render and the interaction:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('filters without render loops', async () => {
const todos = [
{ id: 1, text: 'Write tests', done: true },
{ id: 2, text: 'Ship', done: false },
];
render(<TodoList todos={todos} />);
expect(screen.getAllByRole('listitem')).toHaveLength(2);
await userEvent.click(screen.getByRole('button', { name: 'Done' }));
expect(screen.getAllByRole('listitem')).toHaveLength(1);
expect(screen.getByText('Write tests')).toBeInTheDocument();
});Before the fix, render throws Too many re-renders; after it, both assertions pass. In the browser, the React DevTools Profiler shows one commit per click.
Interview exercise
Why does <button onClick={setCount(count + 1)}> crash with “Too many re-renders” on first render, while <button onClick={() => setCount(count + 1)}> works? Would onClick={setCount} work?
Answer and reasoning
JSX attributes are ordinary JavaScript expressions evaluated while the component renders. setCount(count + 1) is a call expression, so it runs immediately and schedules a state update during render; React re-runs the component, which calls the setter again, and after 25 attempts React throws. The handler passed to the button is the setter’s return value, undefined. The arrow version creates a function during render but does not call it; React stores it and runs it on click, which is an event-time update and perfectly fine.
onClick={setCount} would not crash, because it passes a function reference. But React would call it with the click event object, so count would become a SyntheticEvent, and the next render would hit Objects are not valid as a React child when you try to render {count}. A strong answer also notes that rendering must be pure and that setters schedule work rather than mutate.
Continue learning
Practise with the React interview questions and the React MCQs. Derived state in React explains when a value should not be state, and React state snapshots explains what a setter actually does. If your loop comes from an effect rather than the body, read Maximum update depth exceeded, and if a child is updating its parent during render, see Cannot update a component while rendering a different component. The official guides are Keeping Components Pure and Responding to Events on react.dev.