Skip to content

Reducer Pattern

The reducer pattern combines useReducer with Context to distribute state and dispatch through the tree — Redux-like architecture using only React APIs. Ideal for medium-complexity global state.

Context + useReducer

Place useReducer in a provider component, pass { state, dispatch } through context, and let descendants dispatch actions. Reducers stay pure and testable; components stay thin.

This pattern scales further than raw context + useState but stops short of Redux DevTools and middleware unless you add them yourself.

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD': return { items: [...state.items, action.item] };
    case 'CLEAR': return { items: [] };
    default: return state;
  }
}

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  return (
    <CartContext.Provider value={{ state, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

Consumers call dispatch({ type: 'ADD', item }) — never mutate cart state directly.

Action Design

{ type: 'ADD_TODO', payload: { id, text } }
{ type: 'SET_FILTER', payload: 'completed' }
  • Use consistent action type naming — SCREAMING_SNAKE_CASE is common.
  • Keep payloads serializable for DevTools compatibility.
  • Colocate action creators as functions returning action objects.
  • Split reducers by domain when state grows large.

Reducer Pattern Checklist

Building blocks of context + reducer architecture.

Piece Role
Reducer Pure state transitions
dispatch Trigger actions from UI
Provider Owns reducer state
Context Distributes state + dispatch
Action creators Encapsulate action object shape
Selectors Derive computed state from store

Splitting Reducers

For multiple domains, combine reducers manually or use combineReducers pattern from Redux — or migrate to Redux Toolkit when complexity warrants full tooling.

When to Upgrade to Redux

Consider Redux Toolkit when you need middleware (logging, async), time-travel debugging, large team conventions, or many interdependent slices. The reducer pattern is a stepping stone, not a dead end.

Common Mistakes

  • Dispatching from inside reducers.
  • Putting async fetch logic in reducers instead of thunks/effects.
  • Not memoizing derived selectors causing extra renders.
  • Giant switch statements without slice separation.

Key Takeaways

  • Context + useReducer mimics Redux locally with React builtins.
  • Reducers stay pure; async work lives in effects or thunks.
  • Action creators improve readability and typing.
  • Upgrade to Redux Toolkit when middleware and DevTools become necessary.

Pro Tip

Extract action creators to a cartActions.js file — components dispatch addItem(item) instead of raw objects.