The dispatch function returned by useReducer is not asynchronous — it is a synchronous function call. When you call dispatch(action), React synchronously invokes your reducer with the current state and the action, computes the new state, and schedules a re-render. However, the state variable does not update immediately within the same render cycle. The updated state is only available in the next render.
This behavior is similar to useState's setState — React batches state updates for performance optimization, meaning the component does not re-render immediately after each dispatch call. Instead, React processes all dispatched actions and re-renders once with the final state.
Key Points
dispatch is synchronous: The reducer runs immediately when dispatch is called.
- State update is not immediate in the current render: The state variable still holds the old value until the next render.
- React batches updates: Multiple
dispatch calls within the same event handler result in a single re-render.
- Reducer is a pure function: It computes the new state without side effects.
Example demonstrating that state does not update immediately
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
const handleClick = () => {
dispatch({ type: 'increment' });
console.log(state.count);
dispatch({ type: 'increment' });
console.log(state.count);
};
return (
<button onClick={handleClick}>Increment Twice</button>
);
}
In the above example, even though dispatch is called twice, state.count still reflects the previous value inside the event handler. React batches both dispatches and re-renders the component once with count: 2.
How to read updated state after dispatch
If you need the updated value right after dispatching, you have several options:
- Use
useEffect to react to state changes:
useEffect(() => {
console.log('Updated count:', state.count);
}, [state.count]);
- Compute the next state manually:
const handleClick = () => {
const nextState = reducer(state, { type: 'increment' });
console.log('Next state will be:', nextState.count);
dispatch({ type: 'increment' });
};
- Use
useRef to track the latest state:
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state;
}, [state]);
Note: This behavior is by design in React. The dispatch function itself has a stable identity (it doesn't change between re-renders), which makes it safe to omit from useEffect dependency arrays.