useReducer is React's answer to complex state management. While useState is perfect for simple values, useReducer shines when you have:
- Multiple sub-values that need to update together
- Complex state logic where the next state depends on the previous one
- Related state transitions that should be handled consistently
The Reducer Pattern
A reducer is a pure function that takes the current state and an action, then returns a new state:
tsxtype State = { count: number; step: number; history: number[]; }; type Action = | { type: 'increment' } | { type: 'decrement' } | { type: 'setStep'; payload: number } | { type: 'reset' }; function reducer(state: State, action: Action): State { switch (action.type) { case 'increment': return { ...state, count: state.count + state.step, history: [...state.history, state.count + state.step] }; case 'decrement': return { ...state, count: state.count - state.step, history: [...state.history, state.count - state.step] }; case 'setStep': return { ...state, step: action.payload }; case 'reset': return { count: 0, step: 1, history: [] }; default: return state; } }
Using the Hook
tsxfunction Counter() { const [state, dispatch] = useReducer(reducer, { count: 0, step: 1, history: [] }); return ( <div> <p>Count: {state.count}</p> <p>Step: {state.step}</p> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <input type="number" value={state.step} onChange={(e) => dispatch({ type: 'setStep', payload: Number(e.target.value) }) } /> <button onClick={() => dispatch({ type: 'reset' })}>Reset</button> </div> ); }
Why useReducer Over useState?
- Testability: Reducers are pure functions—easy to test in isolation
- Predictability: All state transitions are explicit and traceable
- Performance: Pass
dispatchto children without worrying about reference changes - DevTools: Works great with debugging tools that can replay actions