skip to content

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%

answer

  1. it runs later, not at call time
  2. render rules apply to it
  3. assume it can run twice
  4. mutating prev breaks the replay

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.

solid answer

~50 s

An updater is not executed when you call the setter — it is queued and invoked later, during the render pass that folds the queue, which puts it firmly inside render. React reserves the right to call it more than once for a single logical update: in development, StrictMode deliberately double-invokes updaters to surface impurity, and a queue can be re-processed when React re-renders a component. So an updater that fires a request, writes to a ref, logs, or increments a module-level counter produces effects an unknown number of times. Worse, mutating `prev` instead of returning a new object breaks the fold itself — later entries and any re-processing then start from an object that has already been changed, and React's identity comparison can no longer tell old from new. The contract is simple: read the argument, return the next value, do nothing else.

go deeper

for a junior

Remember the shape: an updater takes the previous value and returns a new one, and does nothing else. Never push into or otherwise modify the argument you were given.

for a middle

Explain that the function is queued and invoked during render, so render's purity rules apply, and show the difference between returning a new array and mutating the one you received.

for a senior

Diagnose real symptoms from the rule — duplicated requests, a counter moving by two in development, an update that silently does nothing because the same reference came back — and place effects in the handler where they run once.

for a principal

Own it as a codebase-wide invariant: keep StrictMode on so impurity surfaces in development, make 'updaters are pure fold steps' a review rule, and recognise that this contract is what lets React replay and discard renders at all.

## Where the updater actually runs The common mental picture is that `setItems(prev => [...prev, item])` runs the arrow function on the spot and hands the result to React. It does not. The setter appends the *function itself* to the update queue for that state and returns. The function is invoked later, while React folds the queue to compute the next state — which is part of the render pass, not part of your event handler. That single fact carries all the consequences. Render in React is required to be a pure computation from props and state, and the updater has been pulled inside that computation. It inherits render's rules. ## React may call it more than once Two mechanisms make "exactly once" an unsafe assumption: - **StrictMode (development only, React 19).** StrictMode intentionally double-invokes component functions and updater functions so impurity shows up loudly in development instead of subtly in production. If your updater increments a counter outside React, you will see it move by two and blame React — the double call is the diagnostic, not the bug. - **Re-processing of the queue.** React computes the next state by folding the queue from the state the previous render used. If a component is rendered again before that work is committed, the fold can run again from the same starting point. An updater that was written as a pure fold step handles this trivially; one that did something to the outside world does not. So the honest contract is: your updater may run once, twice, or occasionally more, and you may not observe the difference. ## Why mutating `prev` is worse than a mere style issue ```javascript // Broken: mutates the previous state and returns it setItems(prev => { prev.push(item); return prev; }); // Correct: derives a new value, leaves prev untouched setItems(prev => [...prev, item]); ``` The broken version fails in three separate ways at once. First, the fold is no longer replayable: the starting value has been permanently altered, so running it a second time appends the item twice. Second, the returned reference is the same object React already holds, so the identity comparison React performs on the result cannot distinguish "changed" from "unchanged" — the update can be dropped and the UI never updates. Third, any other holder of that array — a memoised child, a value captured by another render's closure — silently observes a value that changed underneath it, breaking the guarantee that a render's snapshot is immutable. The correct version has none of these properties: it reads its argument, allocates a new array, and returns it. Running it twice from the same `prev` yields two equal-but-independent results, which is exactly what makes repeated invocation harmless. ## What counts as impure here Anything observable from outside the function: network calls, `localStorage` writes, mutating a ref's `current`, dispatching to another store, `console.log` you rely on for counting, incrementing a module-scope variable, generating a value from `Math.random()` or `Date.now()` and treating it as stable, and calling another component's setter. The last one deserves a call-out because it looks harmless: enqueueing an update to a *different* state from inside an updater couples two folds together and fires however many times the outer updater runs. ## Where that work belongs instead Side effects triggered by a user action belong in the event handler, next to the setter call, where they run exactly once per interaction: ```javascript function handleAdd(item) { postItem(item); // effect: once per click setItems(prev => [...prev, item]); // pure fold step } ``` If the effect must follow the state landing rather than the click, it belongs in an effect that reacts to the new state, not inside the updater. ## Answering it well in an interview Lead with the mechanism — "it is queued and run during render, possibly more than once" — then give the two failure modes: repeated side effects, and a mutated `prev` that makes the fold non-replayable and defeats the identity comparison. Candidates who only say "React likes immutability" have described the rule; you want to describe the machine that enforces it.

  • Your team sees a counter outside React increment by two per click in development. What do you tell them?
    That the increment is sitting inside a component body or a state updater, and StrictMode is double-invoking it in development to expose the impurity. The fix is to move the increment into the event handler or an effect, not to disable StrictMode — production would then carry a real bug that React had already pointed at.
  • Is calling another state's setter from inside an updater acceptable?
    No. The updater runs during render and can run more than once, so enqueueing from inside it couples two folds and can enqueue repeatedly. Do the second update in the same event handler, or derive the second value during render instead of storing it.
  • Can an updater read a value from a ref?
    Reading a ref during render is already discouraged because the value is not part of the render's inputs and gives you a result the UI has not accounted for; writing one from an updater is worse, since it is a side effect that may run twice. If the value drives the next state, pass it in as an argument from the handler instead.

saying these in an interview costs you the question

  • Says purity is only about React liking immutability
  • Assumes the updater runs exactly once per call
  • Pushes into prev and returns the same array
  • Puts a fetch or log inside the updater
  • Blames StrictMode's double call rather than the impurity

context