useReducer
Manage complex state transitions with a reducer function.
Syntax
const [state, dispatch] = useReducer(reducer, initialState);useReducer is an alternative to useState for state with complex update logic or many related fields. You describe how state changes in a single reducer function.
How it works
- A reducer takes the current
stateand anaction, and returns the next state. - You trigger changes by calling
dispatch({ type: '...' }). - All update logic lives in one place, which is easier to test and reason about.
Example
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'inc':
return { count: state.count + 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'inc' })}>+1</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}When to use it
- A shopping cart manages items through a useReducer so ADD_ITEM, REMOVE_ITEM, and CLEAR_CART actions keep all update logic in one auditable place.
- A multi-step form uses a reducer to handle NEXT_STEP, PREV_STEP, SET_FIELD, and SUBMIT actions so the transition logic is centralised and unit-testable.
- A real-time game state tracks player position, score, and lives in a single reducer so any game event can update multiple fields atomically.
More examples
Counter with useReducer
Defines a reducer with three action types and dispatches them from buttons β the canonical useReducer pattern.
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 };
case 'reset': return { count: 0 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}Cart reducer with payload
Shows a reducer that handles ADD with a payload, REMOVE by filtering on an ID, and CLEAR to reset to empty.
function cartReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, action.item];
case 'REMOVE':
return state.filter(i => i.id !== action.id);
case 'CLEAR':
return [];
default:
return state;
}
}
import { useReducer } from 'react';
function Cart() {
const [items, dispatch] = useReducer(cartReducer, []);
return (
<button onClick={() => dispatch({ type: 'ADD', item: { id: 1, name: 'Book' } })}>
Add Book
</button>
);
}Reducer with complex nested state
Manages a multi-step form with nested state using one reducer, keeping step navigation and field updates together.
const initial = { step: 0, data: { name: '', email: '' } };
function wizardReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return { ...state, data: { ...state.data, [action.field]: action.value } };
case 'NEXT':
return { ...state, step: state.step + 1 };
case 'BACK':
return { ...state, step: Math.max(0, state.step - 1) };
default:
return state;
}
}
import { useReducer } from 'react';
function Wizard() {
const [state, dispatch] = useReducer(wizardReducer, initial);
return <p>Step {state.step}: {state.data.name}</p>;
}
Discussion