React: From Components to Production · State, Events and Effects
useReducer for Complex State
Centralize state transitions with reducers and typed actions.
useReducer manages state with a pure reducer function (state, action) => newState. It's useful when several values change together, when the next state depends on the previous in complex ways, or when you want transitions to be testable outside the component. Type actions as a discriminated union for exhaustive handling.
type State = { items: CartItem[]; coupon: string | null };
type Action =
| { type: "add"; item: CartItem }
| { type: "remove"; id: string }
| { type: "applyCoupon"; code: string };
function cartReducer(state: State, action: Action): State {
switch (action.type) {
case "add":
return { ...state, items: [...state.items, action.item] };
case "remove":
return { ...state, items: state.items.filter((i) => i.id !== action.id) };
case "applyCoupon":
return { ...state, coupon: action.code.toUpperCase() };
}
}
const [cart, dispatch] = useReducer(cartReducer, { items: [], coupon: null });
dispatch({ type: "add", item });
Common pitfalls
- Side effects (fetching, localStorage) inside reducers — keep them pure.
- Mutating state in the reducer.
- Using reducers for trivial single values.
Key points
- Reducers are pure and testable
- Discriminated unions type actions
- Good for related, multi-field state
Further reading: react.dev: useReducer
Practice questions
Why must reducers be pure?
Answer
React may call them more than once (e.g., in StrictMode) and relies on them returning the same output for the same input. Side effects belong in event handlers or effects.
When would you choose useReducer over several useState calls?
Answer
When multiple pieces of state update together, transitions are complex, or you want to test state logic independently of the component.
Open in the app to mark lessons complete, take notes and track your progress.