In a React click handler, calling setNumber(number + 1) three times raises the counter by one, but calling setNumber(n => n + 1) three times raises it by three. Describe what React puts in its update queue for each call and how it computes the next state.
answer
- two kinds of entry in one queue
- a value replaces, a function composes
- the fold starts from the last render's state
- the updater's argument is the queue's running result
basics
~20 sEach call appends an entry to that state's update queue. A plain value entry replaces the state outright, so three identical values collapse to one result; an updater function entry is called with the result of the previous queued entry, so three of them compose.
solid answer
~50 sEvery setter call appends an entry to the queue React keeps for that piece of state on the component's fiber; nothing is computed at call time. On the next render React walks that queue in order, starting from the state the last render used, and applies each entry: a plain value **replaces** the accumulated result, while a function entry is **called with** the accumulated result and its return value becomes the new accumulated result. Three `setNumber(number + 1)` calls all evaluate `number + 1` against the same render snapshot, so the queue holds `1, 1, 1` — three replacements ending at 1. Three `setNumber(n => n + 1)` calls queue three functions, each fed the previous one's output, so the chain runs 0 → 1 → 2 → 3. That is why the updater form is the correct tool whenever the next value depends on the previous one.
code
javascript · 31 linesimport { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
function collapses() {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1); // queue: [1, 1, 1] -> final 1
}
function composes() {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1); // queue: [fn, fn, fn] -> final 3
}
function mixed() {
setNumber(number + 5);
setNumber(n => n + 1); // queue: [5, fn] -> final 6
}
return (
<>
<output>{number}</output>
<button onClick={collapses}>collapses</button>
<button onClick={composes}>composes</button>
<button onClick={mixed}>mixed</button>
</>
);
}go deeper
Recognise the symptom and the fix: when the next value is built from the previous one, pass a function to the setter instead of an expression that reads the state variable.
Walk the queue: three calls, three entries, one fold on the next render. Say which entries replace and which compose, and predict the result of a mixed value-then-updater sequence out loud.
Use it to explain real bugs — several sources updating one state in a tick, a callback enqueueing from an older snapshot — and set a team rule that derived updates always use the function form. Explain why entry count and render count are unrelated.
Judge when many small independent updates to one state signal the wrong shape — a reducer with explicit actions, or split state slices, often models the transitions better than a pile of updater closures folded in queue order.
## The queue is the whole answer A state setter does not compute anything. It appends an entry to an update queue that React keeps per state slot on the component's fiber, and marks the component as needing work. The interesting behaviour all happens later, when React processes that queue to produce the next value. There are exactly two kinds of entry: - **A value.** `setNumber(5)` queues "become 5". When processed, it discards whatever the accumulated result was and replaces it. - **An updater function.** `setNumber(n => n + 1)` queues the function itself. When processed, React calls it with the accumulated result so far and uses the return value as the new accumulated result. ## Processing the queue On the next render, React starts from the state the previous render used and folds the queue over it in call order. It is a reduce: `queue.reduce((acc, entry) => typeof entry === 'function' ? entry(acc) : entry, previousState)`. Nothing more exotic than that. Now the two puzzles fall out. Three plain calls, with `number === 0` in the current snapshot: ```javascript setNumber(number + 1); // number is 0 → queues the value 1 setNumber(number + 1); // number is STILL 0 → queues the value 1 setNumber(number + 1); // number is STILL 0 → queues the value 1 ``` `number` is a constant of the current render (see the snapshot rule), so all three expressions evaluate to `1` at call time. The queue is `[1, 1, 1]`; folding it gives 0 → 1 → 1 → 1. The counter moves by one. Three updater calls: ```javascript setNumber(n => n + 1); // queues a function setNumber(n => n + 1); // queues a function setNumber(n => n + 1); // queues a function ``` The queue is `[fn, fn, fn]`; folding it gives 0 → 1 → 2 → 3. **The key point that separates a good answer from a memorised one: the parameter `n` is not the rendered value — it is the accumulated result of the entries queued before this one.** People often describe it as "the latest state", which is close enough for the simple case but wrong in spirit; it is the *queue's* running result. ## Mixing the two forms Because both kinds live in one ordered queue, mixing them is well defined and a favourite interview probe. With `number === 0`: ```javascript setNumber(number + 5); // queues 5 setNumber(n => n + 1); // queues a function // final state: 6 ``` And the other order: ```javascript setNumber(n => n + 1); // queues a function setNumber(42); // queues 42, discarding the accumulated 1 // final state: 42 ``` A plain value queued after an updater throws away everything the updater computed, because "replace" is exactly what a value entry means. ## Why this matters beyond the puzzle The practical rule: **whenever the new state is a function of the old state, pass a function.** It is not a style preference, it is the only form that is correct when more than one update can be in flight, and it is what makes an update independent of which render enqueued it. A handler that queues `setItems([...items, item])` twice adds one item; queueing `setItems(prev => [...prev, item])` twice adds two. The same reasoning saves code that enqueues from somewhere the render snapshot is old — a callback that outlives the render that created it, or several sources updating the same state in the same tick. The updater form does not read any captured variable, so there is nothing to go stale. One more consequence worth stating out loud: because entries are queued rather than applied, the number of *entries* and the number of *renders* are unrelated. Many setter calls in one tick produce one pass over the queue and one render; that collapsing is why the fold exists at all. ## What a weak answer sounds like "setState is asynchronous, so you have to use the callback form." That describes the symptom without the machine. The precise version is: the setter enqueues; the queue is folded on the next render; a value entry replaces and a function entry composes. Everything else — the collapsing counter, the mixed-order results, why updaters survive stale closures — is derivable from those three sentences.
- Inside setNumber(n => n + 1), what exactly is n?It is the accumulated result of processing every queue entry enqueued before this one, starting from the state the previous render used — not the `number` constant of the render that ran the handler. For the first entry in the queue those two coincide, which is why the shorthand "the latest state" usually works but is not the precise rule.
- With number at 0, what is the final state after setNumber(n => n + 1) followed by setNumber(42)?42. The updater runs first and brings the accumulated result to 1, then the value entry replaces it outright. Value entries discard the accumulated result rather than combining with it, so ordering between the two forms is significant.
- Does using updaters everywhere have a downside?Mainly readability: for a value that does not depend on the previous one — `setStatus('done')` — the updater form adds noise and a pointless closure. The rule is dependence, not habit: pass a function when the next value derives from the old one, pass a value when it does not.
The queue is a stack of instruction cards handed to React. A value card says "make it 5"; a function card says "take whatever is on the pad and add one". React reads them top to bottom in one pass.
saying these in an interview costs you the question
- Says the three calls overwrite each other because React is async
- Thinks the updater argument is the rendered state variable
- Believes each setter call triggers its own render
- Claims a plain value after an updater is merged with it
- Says only class components had this behaviour