What is the purpose of callback function argument format of `setState()` in React and when should it be used?
TL;DR
The callback (or updater function) form of setState — both this.setState(prev => ...) in classes and setX(prev => ...) with useState — guarantees that each update is computed from the latest queued state rather than the value captured in your closure. Use it whenever the next state depends on the previous state, especially when you call the setter more than once in the same event handler or when the update may run after an await/timeout/promise.
// Modern hooks form (preferred)const [count, setCount] = useState(0);const handleClick = () => {setCount((c) => c + 1);setCount((c) => c + 1); // Both run; final count is +2.};
// Legacy class formthis.setState((prevState, props) => ({counter: prevState.counter + props.increment,}));
Purpose of the updater function form of setState
What it is
React's state setters — this.setState in class components and the setX returned by useState in function components — accept either a new value or an updater function. The updater receives the latest queued state (and, for class components, the latest props) and returns the next state.
The community calls this the updater function form (sometimes "functional updater"). Recognising that name is helpful in interviews.
Why it exists: state updates are batched and asynchronous
State setters do not change state immediately. React queues the update and applies it later, then re-renders. Since React 18, this batching is automatic and applies everywhere — event handlers, promises, setTimeout, native event handlers — not only inside React event handlers as in earlier versions.
That means by the time the queued update actually runs, the variable you captured from the previous render may be stale.
The motivating bug — reading state directly between successive calls
The classic mistake is calling the setter more than once based on the current state value:
function Counter() {const [count, setCount] = useState(0);const handleClick = () => {// BUG: each call uses the same closed-over `count` (still 0 on this render).setCount(count + 1);setCount(count + 1);setCount(count + 1);// After re-render, count is 1 — not 3.};return <button onClick={handleClick}>{count}</button>;}
count is captured by the closure when the component rendered. All three calls compute 0 + 1, so React queues 1, 1, 1 and the final state is 1.
The same bug exists in classes — reading this.state.counter between setState calls returns the value from the last render, not the in-flight queued value.
The fix — pass an updater function
function Counter() {const [count, setCount] = useState(0);const handleClick = () => {setCount((c) => c + 1);setCount((c) => c + 1);setCount((c) => c + 1);// Final count is 3 — each updater receives the result of the previous one.};return <button onClick={handleClick}>{count}</button>;}
Each updater receives a snapshot of the latest queued state, not the value from your render closure.
When to use it
- The next state depends on the previous state (counters, toggles, append-to-array, increment-a-map-entry).
- You call the setter more than once in the same handler.
- The update happens after an
await,setTimeout, promise resolution, or subscription callback — by then the closed-over value is almost certainly stale. - Inside
useEffectoruseCallbackwhere omitting the state from deps would otherwise cause stale-closure bugs — using the updater lets you drop the value from the dependency array.
When you do not need it
If the new value does not depend on the previous state — setName('Alice'), setUser(response.data) — passing the value directly is fine and slightly more readable.
Class component equivalent
The same idea applies in class components, where the updater also receives props:
class Counter extends React.Component {state = { counter: 0 };incrementCounter = () => {this.setState((prevState, props) => ({counter: prevState.counter + props.increment,}));};render() {return (<div><p>Counter: {this.state.counter}</p><button onClick={this.incrementCounter}>Increment</button></div>);}}