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.