skip to content

State Updates as a Queue

State in React is a snapshot fixed for the whole render, not a live variable: updates go into a per-fiber queue and are applied when the next render runs. Interviewers love the puzzle this explains — three setCount(count + 1) calls in a row that only increment once.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In a React component, a click handler calls setCount(count + 1) and then console.log(count) on the very next line, and the log still shows the old number. Why does count not change inside that handler?

level: juniorimportance: must knowfreq 82%

answer

  1. think about what the render closed over
  2. state is a snapshot, not a live box
  3. the setter schedules, it never assigns
  4. count is a const inside that render

basics

~20 s

React state is a snapshot fixed for the whole render. count is a const captured when that render ran, and setCount only queues an update for the next render, so lines after it still read the old value.

solid answer

~50 s

Each render of a component produces its own snapshot of state. When that render ran, `count` was destructured out of `useState` as a plain `const` holding the value for **that** render, and nothing can reassign it afterwards — the event handler is a closure created during that render, so it will read the same number for as long as it lives. `setCount(count + 1)` does not write into that variable; it appends an update to the queue React keeps for that state on the component's fiber, and marks the component for re-render. React then processes the queue and calls the component function again, producing a *new* snapshot in which `count` is a different `const`. So the value you logged is not stale by accident — it is exactly the value this render was supposed to see. If you need the new number in the same handler, compute it into a local variable and use that.

go deeper

for a junior

Be able to say plainly that the state variable is a constant for the current render and the setter only schedules an update. Show that you would compute the new value into a local variable when you need it immediately.

for a middle

Explain the mechanism: the setter appends to the update queue for that state and marks the component dirty; the next render creates fresh bindings. Point out that handlers are closures over the render that created them.

for a senior

Show why the snapshot rule exists — it keeps a render a pure function of props and state, so handlers and effects always agree with the frame on screen. Diagnose colleagues' 'setState is async' hand-waving with the closure explanation.

for a principal

Frame it as an API design tradeoff: immutable per-render snapshots buy referential reasoning, replayable renders and concurrent rendering, at the cost of surprising newcomers. Contrast with frameworks that expose mutable reactive state and say what each model gives up.

## The mental model that breaks Most people read `const [count, setCount] = useState(0)` as "count is a variable holding my state, setCount writes to it." That model predicts that reading `count` after calling the setter shows the new number. It never does, and the reason is not timing tricks or asynchrony in the network sense — it is that **state in React is a snapshot, not a live cell**. ## What one render actually produces When React renders a component, it calls your component function. That call creates a fresh set of local bindings. `count` is a `const` initialised to whatever value React hands back for this render. Every piece of JSX, every event handler, every callback created during that call closes over *that* binding. ```javascript import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); console.log(count); // the value of THIS render, always } return <button onClick={handleClick}>{count}</button>; } ``` The handler that runs when you click is the function object created during the render that produced the currently-shown UI. Inside it, `count` cannot be anything other than the number that render saw. There is no mechanism by which a `const` created in a finished function call is later reassigned, and React does not try to invent one. ## What the setter does instead `setCount(next)` does two things. It appends an update — a record saying "this state should become `next`" — to the update queue React keeps for that state slot on the component's fiber, and it schedules the component for re-render. It returns `undefined`; there is nothing useful to await and nothing to read back. On the next render, React walks that queue, applies the updates in order to produce the new value, and calls your component function again. That call creates a **new** `count` const with the new value, new handlers closed over it, and new JSX. The old snapshot is not mutated; it is replaced. So the sequence for one click is: handler runs to completion with the old snapshot → React processes the queue → component function runs again with a new snapshot → the DOM shows the new number. ## Why this design and not a live variable A snapshot makes rendering a pure function of props and state: given the same inputs, a render produces the same output, and everything created inside it — handlers, effect closures, memo values — is guaranteed to agree with the UI the user is currently looking at. If `count` could change under your feet mid-handler, a handler that reads it twice could see two different values and produce output that matches no rendered frame. The snapshot rule is what makes "what is on screen" and "what my code sees" the same thing. ## Getting the new value when you actually need it Three honest options: 1. **Compute it once into a local.** `const next = count + 1; setCount(next); console.log(next); sendAnalytics(next);` — this is the common case and it is not a workaround, it is the correct expression of intent. 2. **Use the updater form** when the new value depends on the previous one and several updates may be in flight: `setCount(c => c + 1)`. React calls that function while processing the queue, so it receives the value produced by earlier queued updates rather than the render snapshot. 3. **React to the change after it lands** — read the new value in the next render, where it simply *is* `count`. ## The misdiagnoses to avoid Wrapping the read in `setTimeout` does not help: the timeout callback is still a closure over the same render's `count`, so it prints the same old number, just later. Neither does calling the setter twice, or `await`ing it — the setter is not a promise. And the behaviour has nothing to do with development mode or with which React version you are on: it is the core semantics of state, unchanged in React 19. A useful phrase for interviews: *the setter schedules, it never assigns.* Everything else follows from that plus the fact that each render gets its own constants.

  • If you need the new value to send to an analytics call in the same handler, what do you do?
    Compute it once into a local variable and use that for both the state update and the call: `const next = count + 1; setCount(next); track(next);`. Do not try to read it back from the state variable — that binding belongs to the current render and will never change. Reading it in the next render also works if the call can wait.
  • Does wrapping the read in setTimeout let you see the updated value?
    No. The timeout callback is created during the same render and closes over the same `count` binding, so it logs the same old number a few milliseconds later. Delay does not change which snapshot a closure captured; only a new render creates a new binding.
  • The setter returns something — can you use it?
    It returns `undefined`. There is no new state handed back and nothing to await; a state setter is not a promise, so `await setCount(1)` resolves immediately with `undefined` and still leaves the current render's variable untouched.

A render is a photograph of state, not a window onto it. Calling the setter tells the camera to take a new photo; it does not retouch the print you are already holding.

saying these in an interview costs you the question

  • Says setState is async because it talks to the server
  • Thinks reading inside setTimeout reveals the new value
  • Believes React reassigns the count variable in place
  • Calls the setter twice to force the value through
  • Claims you can await the setter to get new state

context

open as a page

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.

level: middleimportance: must knowfreq 72%

basics

~20 s

Each 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.

open as a page

A React component's status state is the string 'idle', and a click handler calls setStatus('idle') again with the identical value. Does React re-render the component, and what comparison decides that?

level: middleimportance: should knowfreq 48%

basics

~20 s

React compares the next state with the current one using Object.is. Because the values match, it bails out and skips re-rendering the subtree — though it may still run this component's function once before bailing out, so never rely on that render not happening.

open as a page

In React, the function passed to a state setter — as in setItems(prev => [...prev, item]) — is required to be pure. What does React do with that function that makes purity a hard requirement rather than a style rule?

level: seniorimportance: should knowfreq 42%

basics

~20 s

React does not run the updater at call time; it stores it and runs it while processing the update queue during render, possibly more than once. Anything impure inside — a side effect, or mutating the previous state — fires unpredictably or corrupts the replay.

open as a page