codejump

AcademyReact

How useState Batches and Updates

Why the value you read right after calling a setter is still stale, when the updater-function form is required, and how React batches several setState calls into one render.

3 min readUpdated

3 exercises

Waiting at the end of this page.

useState returns a snapshot, not a live variable

Calling the setter React hands you from useState does not mutate the variable in place. It schedules a re-render, and on that re-render the component function runs again from the top, with a new count bound to the new value. The old count — the one your current closure holds — never changes.

JSX
function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    console.log(count); // still the OLD value
  }

  return <button onClick={handleClick}>{count}</button>;
}

Click once: handleClick runs with count fixed at 0 for its entire execution. setCount(1) is queued, but count inside that same call is still 0 — logging it right after proves nothing changed synchronously. The button then re-renders with count = 1, and a fresh handleClick closure is created that will see 1 next time. Nothing is broken; the variable you are holding was never going to update itself.

The bug this causes: calling the setter more than once

JSX
function handleTripleClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}
// starts at 0, ends at 1 — not 3

All three lines run inside the same call, so count is the same stale 0 in every one of them. Each line says "set the count to 0 + 1", three times. React does not add three separate increments; it applies the last write, which is 1 computed from the same starting value the other two also used.

The fix: the updater-function form

JSX
function handleTripleClick() {
  setCount((c) => c + 1);
  setCount((c) => c + 1);
  setCount((c) => c + 1);
}
// 0 -> 3

Passed a function instead of a value, React queues each one and runs them in order against whatever the previous updater in the queue produced, not against the stale closure variable. The first receives the current state, the second receives the first's return value, and so on. This is not a style preference — it is the only form that is correct whenever the next state depends on the previous one and more than one update might land before a render happens.

setCount(count + 1)     // "set it to this value I computed once"
setCount(c => c + 1)    // "whatever it ends up being, add one"

Use the value form when the new state does not depend on the old one (setCount(0), resetting to a fixed value). Use the updater form whenever it does.

Batching: several setState calls, one render

JSX
function handleClick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  setName('done');
}

React does not re-render after each of these three calls. It collects all the state updates that happen inside one event handler and applies them together, producing exactly one re-render with all three changes visible at once. This matters for two reasons: performance — three renders' worth of work collapses into one — and correctness of your mental model — you can write several related updates in one handler without worrying that the component will briefly render in a half-updated, inconsistent state between them.

Batching used to stop at the edge of React's own event handlers — an update inside a raw setTimeout or a fetch callback triggered an immediate render per call. Since React 18, updates are batched everywhere by default, including inside timeouts, promises and native event listeners, so the rule above now holds almost everywhere you'll write state updates.

What to say in an interview

Two sentences cover it: useState gives you a value fixed for the whole render, and a setter that schedules the next one — reading state right after calling the setter never shows the new value, because there is no new value yet, only a scheduled one. And when the next state depends on the current one, pass a function to the setter, because the value you have in scope may already be stale by the time your update is actually applied.

Now practice it

2 challenges · 1 quiz

Reading this page is the cheap half. These are the exercises that make you use it.