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