Pass a function to the setter when the new value depends on the old one. React calls it with the latest state, however many updates are queued.
setCount((c) => c + 1);
The plain form reads count from the render the handler was created in, so two of them in a row both see the same number:
setCount(count + 1);
setCount(count + 1); // still count + 1, not count + 2
Where it bites
- A handler that updates twice. Increment then also log or check a bound, both against
count, and the second write clobbers the first. - Callbacks that outlive a render. A
setTimeout, a WebSocketonmessageor a resize listener created once holds thecountfrom that first render forever. The updater form reads live state instead. - Arrays and objects.
setItems((items) => [...items, next])is safe to call from two places in the same tick;setItems([...items, next])from two places loses one.
When the plain form is fine
Setting to a value that doesn’t depend on the previous one: setOpen(true), setQuery(e.target.value). An updater there adds nothing.
Gotchas
- Updaters must be pure. React may call one twice in Strict Mode, and it must return a new value rather than mutating the old one:
items.push(x)inside an updater is a bug even if it seems to work. - The state isn’t updated when the setter returns. Reading
countright aftersetCountgives the old value; use the value you just computed, or an effect. - React 18 batches every update in a handler, a promise or a timeout into one render, so three
setCount((c) => c + 1)calls produce one render with+3.