Functional Updates

Because count is frozen for the life of a render, calling setCount(count + 1) twice in one handler asks twice for the same result. Pass a function instead and React 7,897 calls it with the value produced by everything already queued, so the increments compose.

Two updates in one handler: replacement versus updaterJavaScript
const [count, setCount] = useState(0);
function twiceByValue() {      // both calls queue "replace with 0 + 1"
  setCount(count + 1);
  setCount(count + 1);         // count is still 0 here, so this ends at 1
}
function twiceByUpdater() {    // each updater receives the pending value
  setCount(c => c + 1);
  setCount(c => c + 1);        // 0 -> 1 -> 2
}
function mixed() {
  setCount(count + 1);         // queue: replace with 1
  setCount(c => c + 1);        // queue: 1 -> 2
  setCount(count + 1);         // queue: replace with 1 again, so it ends at 1
}
The queue React builds for one handler, and the value it ends on
The queue React builds for one handler, and the value it ends on

Since React 18 the queue is flushed once per event and also once per promise callback, timeout and native handler — automatic batching applies everywhere, not only inside React's own handlers. Wherever two updates to the same state can land in one batch, the updater form is the safe one.

Updaters have a second use: they let a child change a parent's state without knowing the current value. <Stepper onBump={setCount} /> gives the child a setter it calls as onBump(n => n + 1); onBump(count+1) would fail, because count is not in the child's scope at all.