Back to the Library

React Too Many Re-renders - setState Called in the Render Body

5 min read
“The page loads and the component is already gone. No click, no input, nothing. React hit its re-render limit before the first frame ever painted - because something in the component body called setState during render.”

In This Post

What "Render Purity" Means in PracticeWhat the Broken Code Looks LikeThe Fix: Pass the Initial Value to useStateThe Lazy Initializer FormOther Ways This Bug Shows UpPractice These Patterns

Practice This Pattern

White Belt

Notification Badge Never Appears

A notification badge that should show an unread count - but the component goes blank the moment the page loads because setCount is called directly in the render body instead of being passed as the useState initial value.

+10 KI
Enter the Dojo
BugDojo
BlogFAQ

© 2026. Carved in code.

The page loads. The component area is blank. You didn't click anything. There was no user interaction. The component simply never appeared.

Open the console:

Too many re-renders. React limits the number of renders to prevent an infinite loop.

React terminated your component before it could produce any visible output, because the component triggered its own re-render before it even finished rendering the first time.

What "Render Purity" Means in Practice

React expects your component function to be pure - given the same inputs (props and state), it should return the same output (JSX) every time, with no side effects during the calculation. It can read state, it can compute values, it can build JSX. It should not change state while doing any of that.

Calling a state setter - setState, setCount, setUser, anything from a useState pair - is a side effect. It tells React "schedule a re-render with this new value." That scheduling is fine in an event handler (runs after render), fine in a useEffect (runs after render). In the render body itself, it fires during render, which immediately queues another render, which calls the setter again, which queues another render. React cuts this off at a hard limit and throws rather than let it run forever.

What the Broken Code Looks Like

const INBOX_COUNT = 5;
 
export default function NotificationBadge() {
  const [count, setCount] = useState(0);
  setCount(INBOX_COUNT);
 
  return (
    <div>
      <p>Unread: {count}</p>
      <button onClick={() => setCount((c) => c - 1)}>Mark as Read</button>
    </div>
  );
}

setCount(INBOX_COUNT) sits directly in the component body, between the useState call and the return. Every time NotificationBadge renders, this line runs. It calls setCount(5), which schedules a re-render. That re-render runs the body again. setCount(5) fires again. Another re-render. React hits its limit and throws - all before the JSX ever makes it to the screen.

Symptom

The component area is blank from the moment the page loads. No partial render, no flash of content, no recovery. The crash happens before anything is committed to the DOM - there is nothing to show and nothing to interact with.

The intent behind this code is understandable: "I want to initialize count to INBOX_COUNT when the component loads." That's a correct goal. The problem is using setCount to do it. useState already accepts the initial value as an argument - that's exactly what the argument is for.

The Fix: Pass the Initial Value to useState

const INBOX_COUNT = 5;
 
export default function NotificationBadge() {
  const [count, setCount] = useState(INBOX_COUNT);
 
  return (
    <div>
      <p>Unread: {count}</p>
      <button onClick={() => setCount((c) => c - 1)}>Mark as Read</button>
    </div>
  );
}

useState(INBOX_COUNT) sets count to 5 on the first render and never runs again. No setter is called, no re-render is scheduled, and the component renders cleanly - Unread: 5 on the first paint, clicking "Mark as Read" decrements correctly from there.

The setCount call and the useState argument are not two ways of doing the same thing. useState(value) sets the initial state once, at component birth. setCount(value) schedules a re-render with a new state value. The first is initialization. The second is an update. Calling an update setter during render is the bug.

The Sensei's Hint

If you want state to start at a value that isn't a literal - a prop, a constant, a computed expression - pass it directly to useState: useState(props.initialCount), useState(items.length), useState(INBOX_COUNT). The argument runs once, at mount, and is ignored on every subsequent render. No setter needed, no effect needed.

The Lazy Initializer Form

If the initial value is expensive to compute - parsing a large JSON string, reading from localStorage, running a heavy calculation - you can pass a function to useState instead of a value. React calls it once, on mount, and uses the return value as the initial state:

const [data, setData] = useState(() => JSON.parse(rawString));

This is the "lazy initializer" form. The function is called once and never again, which makes it safe for expensive operations. The key difference from the bug above: this is a function passed to useState, called by React at the right time. Calling setData directly in the render body is still wrong regardless of how that call is wrapped.

Other Ways This Bug Shows Up

The render body / setter combination is the most common form, but the same crash happens any time a setter is called unconditionally during render:

// crashes - setter called on every render
const [doubled, setDoubled] = useState(0);
setDoubled(count * 2); // move this to a useEffect or just compute it inline
// crashes - same thing, just structured differently
if (someCondition) {
  setError("something went wrong"); // called during render
}

Both of these have the same loop: render calls setter, setter schedules render, render calls setter. The fix is always the same direction - move the setter call out of the render body and into the place where it belongs: an event handler for user-triggered updates, a useEffect for synchronization with external things, or a plain const expression for values that can be computed inline.

Constraints

If a value can be derived directly from existing state - count * 2, items.length, a formatted string from a name - compute it as a const in the render body, not via a setter. A derived const recalculates during render, which is correct. A setter during render causes a loop. The distinction is: reading and computing is fine; scheduling a state update is not.

Practice These Patterns

"Notification Badge Never Appears" is the pattern in its minimal form - one constant, one misplaced setter, instant crash on mount. The fix is literally moving the value from setCount(INBOX_COUNT) to useState(INBOX_COUNT) - one change, nine characters different. Work through it to build the habit of asking "does this setter call belong here, or is there somewhere better for it to live?"