useReducer takes a reducer function, (state, action) => newState, and an initial state, returning the current state and a dispatch function used to trigger updates by sending an action object, typically with a type field describing what happened. Rather than scattering multiple related useState calls and update logic across a component, useReducer centralizes all the state transition logic into one single reducer function, which is especially useful when a piece of state has several related sub-values that tend to update together, or when the next state genuinely depends on complex logic based on the previous state and the specific action that occurred.
1Understanding useReducer
useReducer takes a reducer function, (state, action) => newState, and an initial state, returning the current state and a dispatch function used to trigger updates by sending an action object, typically with a type field describing what happened. Rather than scattering multiple related useState calls and update logic across a component, useReducer centralizes all the state transition logic into one single reducer function, which is especially useful when a piece of state has several related sub-values that tend to update together, or when the next state genuinely depends on complex logic based on the previous state and the specific action that occurred.
Reach for useReducer over several separate useState calls specifically when state updates involve complex logic, several related sub-values that change together, or when the exact same reducer logic could reasonably be tested independently of any particular component.
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}2Practical Example
Here is a real-world application of useReducer showing how it is used in production React code.
function formReducer(state, action) {
switch (action.type) {
case 'field_changed':
return { ...state, [action.field]: action.value };
case 'reset':
return { name: '', email: '' };
default:
return state;
}
}
const [form, dispatch] = useReducer(formReducer, { name: '', email: '' });
dispatch({ type: 'field_changed', field: 'name', value: 'Ana' });3Best Practices
Follow these guidelines when working with useReducer:
1. Use useReducer instead of multiple related useState calls when several pieces of state tend to update together in response to the same events
2. Keep the reducer function pure, computing and returning a new state object based only on its state and action arguments, with no side effects performed inside it
3. Give dispatched action objects a clear, descriptive type field, and any needed extra data as additional properties, making the reducer's switch/if-else logic easy to follow
Tip: Reach for useReducer over several separate useState calls specifically when state updates involve complex logic, several related sub-values that change together, or when the exact same reducer logic could reasonably be tested independently of any particular component.
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}4Lazy Initialization
useReducer accepts an optional third argument, an init function: useReducer(reducer, initialArg, init). React calls init(initialArg) exactly once, on the first render, to compute the actual initial state — useful when that computation is expensive or depends on props, since it avoids re-running the logic on every render.
function init(count) {
return { count, history: [] };
}
useReducer(reducer, initialCount, init);5Dispatch Has a Stable Identity
The dispatch function returned by useReducer never changes across re-renders — React guarantees the same reference every time. It's safe to omit from a useEffect or useCallback dependency array, and it can be passed down to deeply nested or memoized children without causing them to re-render just because a 'new' function reference showed up.
const [state, dispatch] = useReducer(reducer, init);
// dispatch is the SAME function reference on every render