skip to content

In a React component with `const [count, setCount] = useState(0)`, a click handler calls `setCount(count + 1)` three times in a row, yet each click only raises the count by one. Explain why, and what to write instead.

level: middleimportance: must knowfreq 78%

answer

  1. the handler closed over one value
  2. all three requested the same number
  3. let React hand you the latest
  4. derive from previous, pass a function
  5. batching explains renders, not values

basics

~20 s

All three calls read the same count value from the current render, so all three request the same next value and the last one wins. Passing an updater function, setCount(c => c + 1), makes each call build on the previous result, giving plus three.

solid answer

~50 s

`count` is fixed for the render that created the handler, so with `count` at 0 all three calls are literally `setCount(1)` — React records three requests for the same value and renders once, showing 1. The fix is the updater form: `setCount(c => c + 1)` three times. React applies the queued updaters in order when it renders, handing each one the result of the previous, so you get 3. The rule I follow is: whenever the next state is derived from the current state, pass a function rather than a computed value — that is exactly the case where a snapshot can be stale. Note the single re-render is separate from the wrong number: since React 18 updates are batched everywhere, including in promises and timeouts, so three calls always produce one render. Batching explains the one render; the snapshot explains the one increment.

code

jsx · 25 lines
jsx
import { useState } from 'react';

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

  function addThreeWrong() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  function addThreeRight() {
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
  }

  return (
    <>
      <output>{count}</output>
      <button onClick={addThreeWrong}>+1 (three stale calls)</button>
      <button onClick={addThreeRight}>+3 (three updaters)</button>
    </>
  );
}

go deeper

for a junior

Recall the two shapes of the setter — a value or a function — and that the function form is what you use to increment. Being able to write setCount(c => c + 1) confidently is most of what is expected.

for a middle

Explain the mechanism: the handler captured the state value from its render, so three computed calls all request the same number, while queued updaters each receive the result of the previous one.

for a senior

Show where stale snapshots actually bite in production — intervals, debounced handlers, async continuations after await — and state the reviewable rule that state derived from previous state is always written as an updater.

for a principal

Own the separation of concerns you are teaching the team: batching is a scheduling property, snapshots are a correctness property, and conflating them produces defensive flushSync calls and refs that erode the model.

## The snapshot model is the whole answer Every render of a component is a separate call to the function, and `useState` returns the value React currently holds. That binding is fixed for the whole render, and every closure created during that render — including your `onClick` handler — captures it. So in the render where `count` is 0: ```jsx function handleClick() { setCount(count + 1); // setCount(0 + 1) setCount(count + 1); // setCount(0 + 1) <- count is still 0 setCount(count + 1); // setCount(0 + 1) } ``` Nothing in the handler can change `count`; it is a constant for that render. All three calls compute 1 and ask React to make the state 1. React honours all three, in order, and the result is 1. ## What the updater form changes The setter accepts either a value or a function. When you pass a function, React treats it as an **updater**: instead of "make the state this", it means "make the state whatever comes out of applying this to the state you have when you get here". React applies queued updaters in call order when it computes the next render: ```jsx function handleClick() { setCount(c => c + 1); // c is 0 -> 1 setCount(c => c + 1); // c is 1 -> 2 setCount(c => c + 1); // c is 2 -> 3 } ``` The parameter is not the render's `count`; it is the state as of that point in the applied sequence. That is why the updater is immune to a stale snapshot and the computed value is not. ## The rule to state in an interview **If the next state is derived from the current state, pass a function.** `setCount(c => c + 1)`, `setItems(prev => [...prev, item])`, `setOpen(o => !o)`. If the next state is independent of the old one — `setName(inputValue)`, `setError(null)` — passing the value directly is fine and reads better. This matters far beyond three calls in a row. Any callback that outlives the render that created it holds a stale snapshot: an interval, a debounce, a WebSocket handler, an async continuation after `await`. All of them are correct with an updater and quietly wrong with a computed value: ```jsx useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(id); }, []); // with setCount(count + 1) this would stick at 1 ``` ## Updaters must be pure React may call an updater more than once (in development it deliberately double-invokes to surface impurity). An updater must compute a new value from its argument and do nothing else — no fetching, no logging you count on, no mutating the argument. `setItems(prev => { prev.push(x); return prev; })` is wrong twice over: it mutates and it returns the same reference. ## Batching is a separate fact Candidates often blame batching for the wrong number. Batching is why you get **one render** instead of three, and since React 18 it applies everywhere — event handlers, timeouts, promise callbacks, native handlers — not just in React events. But batching does not change the values you queued: three updaters still yield 3 inside one batched render. Keep the two explanations apart, because an interviewer who hears "batching swallowed two of my updates" knows the model is fuzzy. ## Reading state after setting it `setCount(c => c + 1); console.log(count);` logs the old number, and there is no callback argument to the setter that hands you the new one (that is class-component `setState`, not the hook). If you need the value you just computed, compute it into a local first: ```jsx const next = count + 1; setCount(next); send(next); ``` If the value depends on queued updates you cannot see, do the work in an effect keyed on the state instead. ## The compact answer Say: the handler closed over `count` from its render, so three calls all requested the same value; pass `setCount(c => c + 1)` so each call receives the result of the last; and separately, the three calls always collapse into one render because updates are batched.

  • What exactly is the argument React passes to the function in setCount(c => c + 1)?
    The state as of that point in the applied sequence of updates — the result of every updater queued before it, starting from the current state. It is not the `count` binding from the render that created the handler, which is precisely why the updater form is immune to a stale snapshot.
  • Is there any reason to prefer setCount(value) over the updater form?
    Yes, when the next state does not depend on the old one: `setName(input)` or `setError(null)` read more directly and cannot be stale, since nothing is derived. Reach for the updater specifically when you compute the next state from the previous one, or when the call sits inside a callback that outlives its render.
  • A setInterval created in an empty-dependency effect calls setCount(count + 1) and the counter sticks at 1. Same bug?
    Same bug, worse blast radius. The interval callback captured `count` from the mount render and never sees another value, so it requests 1 forever. `setCount(c => c + 1)` fixes it without re-creating the interval, because the updater gets the current state from React rather than from the closure.
  • Can the updater function do anything besides compute the next value?
    No — it must be pure. React can call it more than once, and deliberately double-invokes updaters in development to surface impurity, so side effects would run twice and mutation of the argument would corrupt state. Compute and return a new value; put side effects in the handler or an effect.

saying these in an interview costs you the question

  • Says batching threw away two of the three updates
  • Thinks setCount is asynchronous so the calls race each other
  • Believes await between the calls would fix the count
  • Claims setCount takes a second callback argument with the new value
  • Uses an updater that mutates and returns the previous value

context