0Pricing
Frontend Academy · Lesson

useReducer for Complex State

Model state transitions with a reducer function, dispatch action objects, and prefer useReducer over useState when state logic grows complex.

useReducer for Complex State is a free Frontend Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Frontend Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

When useState Isn't Enough

When state transitions involve complex logic, multiple related values, or conditions where the new state depends on the previous state in non-trivial ways, useReducer is a cleaner choice than multiple useState calls.

Reducer Function Pattern

A reducer is a pure function: (state, action) => newState. It takes the current state and an action object, and returns the next state. No mutations — always return a new object.

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset'; payload: number };

function counterReducer(state: number, action: Action): number {
  switch (action.type) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'reset':     return action.payload;
    default: return state;
  }
}

useReducer Syntax

useReducer(reducer, initialState) returns the current state and a dispatch function. Call dispatch with an action object to trigger a state transition.

function Counter() {
  const [count, dispatch] = useReducer(counterReducer, 0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
    </div>
  );
}

Typed Actions — Discriminated Unions

Use TypeScript discriminated union types for actions to get exhaustive type checking in the reducer switch.

type CartAction =
  | { type: 'ADD_ITEM'; item: CartItem }
  | { type: 'REMOVE_ITEM'; id: string }
  | { type: 'SET_QUANTITY'; id: string; qty: number }
  | { type: 'CLEAR' };

Complex State Example: Shopping Cart

A shopping cart with add, remove, and quantity update operations is a perfect useReducer use case.

interface CartState {
  items: CartItem[];
  total: number;
}

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD_ITEM': {
      const exists = state.items.find(i => i.id === action.item.id);
      if (exists) {
        const items = state.items.map(i =>
          i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
        );
        return { ...state, items, total: calcTotal(items) };
      }
      const items = [...state.items, { ...action.item, qty: 1 }];
      return { ...state, items, total: calcTotal(items) };
    }
    case 'CLEAR': return { items: [], total: 0 };
    default: return state;
  }
}

Dispatch Is Stable

The dispatch function is stable — it doesn't change between renders. Safe to pass as a prop or use as a useEffect dependency without triggering re-runs.

Combining useReducer + useContext

A powerful pattern: a context provides both the state and dispatch, letting any consumer read state or dispatch actions without prop drilling.

const CartContext = createContext<{ state: CartState; dispatch: Dispatch<CartAction> } | null>(null);

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

useReducer vs useState

Choose useReducer when: state has multiple sub-values, next state depends on previous in complex ways, transitions are many and explicitly named, or you want to test state logic separately from the component.

Testing Reducers in Isolation

Because reducers are pure functions, they're trivially testable without rendering any component.

test('increment action increases count', () => {
  expect(counterReducer(0, { type: 'increment' })).toBe(1);
  expect(counterReducer(5, { type: 'increment' })).toBe(6);
});

test('reset action sets count to payload', () => {
  expect(counterReducer(42, { type: 'reset', payload: 0 })).toBe(0);
});

Immer for Immutable Updates

The immer library lets you write mutating-looking code that produces new immutable state. Useful for complex nested object updates in reducers.

Quick Check

What is the function signature of a reducer?

Recap: useReducer

(state, action) => newState — pure, no mutations. useReducer(reducer, initial) returns [state, dispatch]. Use discriminated union action types for safety. dispatch is stable. Great for complex state with many transitions. Combine with useContext for global state management. Test reducers independently as pure functions.

Frequently asked questions

Is the “useReducer for Complex State” lesson free?

Yes — the full text of “useReducer for Complex State” is free to read here on the web, and the Frontend Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Frontend Academy course, upgrade to CoddyKit PRO.

What will I learn in “useReducer for Complex State”?

Model state transitions with a reducer function, dispatch action objects, and prefer useReducer over useState when state logic grows complex. You practise Frontend Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Frontend Academy?

No prior experience is required. Frontend Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “useReducer for Complex State” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Frontend Academy lesson?

Yes. Every Frontend Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. useContext for Global State
  2. useReducer for Complex State
  3. useMemo and useCallback for Performance
  4. Custom Hooks: Extracting Reusable Logic
← Back to Frontend Academy