Queueing multiple state updates with updater functions
When you need to queue multiple state updates in the same event handler, use an updater function instead of passing a value. Pass a function that receives the previous state and returns the new state. For example, use setScore(s => s + 1) instead of setScore(score + 1). This allows React to batch multiple updates and apply them sequentially based on the previous state.
Multiple setState calls with values don't queue
Calling setScore(score + 1) multiple times in the same function does not increment the score multiple times because each call uses the same score value from the snapshot. Since score continues to be 0 in the event handler, three calls to setScore(score + 1) result in setScore(0 + 1) three times, which only sets the score to 1. Use updater functions instead to queue multiple updates.
Copy Set before mutating to update state
When updating a Set in state, create a copy first using new Set(currentSet), then modify the copy, then pass it to the setState function. Do not mutate the Set directly. This preserves React's immutability requirement.
Custom useReducer implementation code
import { useState } from 'react';
export function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
useReducer dispatch queuing with updater function
A more accurate implementation of dispatch uses an updater function: setState((s) => reducer(s, action)). This approach is necessary because dispatched actions are queued until the next render, similar to how updater functions work in queueing state updates.
useReducer custom hook implementation
A custom useReducer hook can be implemented by creating a function that takes a reducer and initialState. It uses useState internally to manage state, and returns an array with the current state and a dispatch function. The dispatch function takes an action, passes it with the current state to the reducer, and updates state with the result.
Using updater functions with async operations
When using async operations like setTimeout or await, state values captured before the async call become stale. Use updater functions instead of captured state values to ensure you're working with the latest state. For example, use setPending(p => p + 1) instead of setPending(pending + 1) to correctly reference the current state when the async operation completes.
Naming convention for updater function arguments
Common naming conventions for updater function parameters: use the first letter(s) of the state variable name (e.g., setEnabled(e => !e), setFriendCount(fc => fc * 2)), or use the full variable name (e.g., setEnabled(enabled => !enabled)), or use a prefix like prevEnabled (e.g., setEnabled(prevEnabled => !prevEnabled)).
React does not batch across multiple intentional events
React only batches state updates within a single event handler. Each separate click event is handled independently without batching. This ensures that if the first click disables a form, the second click will not submit it.
React batches state updates within event handlers
React waits until all code in an event handler has run before processing state updates. Multiple setState calls in a single event handler are batched together, resulting in only one re-render after the event handler completes. This improves performance and avoids intermediate "half-finished" renders.
Multiple setState calls with the same value replace each other
When you call setNumber(number + 1) three times in a row within the same event handler, all three calls use the same snapshot value of number from the render, so they all queue the same value and only the last one takes effect. The state only increments once, not three times.
Updater function for multiple state updates to same variable
To update the same state variable multiple times before the next render, pass an updater function like n => n + 1 instead of passing the next state value directly. Updater functions receive the most recent state value in the queue and return the new state. React processes all queued updater functions in order during the next render.
Updater function example for multiple increments
Example showing how to increment a counter three times in one event handler:
```js
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
)
}
```
This correctly increments the counter by 3 because each updater function receives the result of the previous one.
State queue processing with mixed updates and replacements
When a queue contains both replacement values and updater functions, React processes them in order. A replacement value (like setNumber(5)) discards everything previously queued and sets the state to that value. Subsequent updater functions operate on the replaced value. For example: setNumber(number + 5), setNumber(n => n + 1), setNumber(42) results in final state 42.
State queue processing table example
When calling setNumber(0 + 5) then setNumber(n => n + 1) on initial state 0:
| queued update | n | returns |
|---|---|---|
| "replace with 5" | 0 (unused) | 5 |
| n => n + 1 | 5 | 6 |
Final state is 6.
Updater functions must be pure
Updater functions passed to state setters must be pure functions. They must only return the new state value based on their argument. They should not set state, run side effects, or cause other mutations. In Strict Mode, React runs each updater function twice to help detect impure functions.
Use conditional returns to prevent invalid state changes
You can use early returns in event handlers to prevent invalid operations. For example, in tic-tac-toe, check if a square is already filled with if (squares[i]) { return; } before updating state. This prevents overwriting existing moves.
Batch updates from different components
React batches updates from different components together, so calling setState in a child event handler along with setState in the parent callback happens in a single render pass.