useReducer

State Logic Lives in a Function, Not in Handlers

useReducer

`useReducer` is `useState`'s big sibling — for state where the next value depends on what kind of update is happening.

5 min read Level 3/5 #react#useReducer#hooks
What you'll learn
  • Move complex state logic into a reducer
  • Dispatch actions instead of calling setters
  • Recognize when to upgrade from `useState`

useState is great when state is simple. When state has many related fields, or transitions are complex, useReducer keeps everything in one place.

The Shape

function reducer(state, action) {
  switch (action.type) {
    case "add":      return [...state, action.item];
    case "remove":   return state.filter(x => x.id !== action.id);
    case "clear":    return [];
    default:         return state;
  }
}

function TodoList() {
  const [items, dispatch] = useReducer(reducer, []);

  return (
    <>
      <button onClick={() => dispatch({ type: "add", item: { id: 1, text: "Hi" } })}>
        Add
      </button>
      <button onClick={() => dispatch({ type: "clear" })}>Clear</button>
      <ul>
        {items.map(t => (
          <li key={t.id}>
            {t.text}
            <button onClick={() => dispatch({ type: "remove", id: t.id })}>x</button>
          </li>
        ))}
      </ul>
    </>
  );
}

The reducer is a pure function: (state, action) => newState. The component dispatches actions; the reducer decides how state changes.

Why It’s Sometimes Better

  • All state transitions are in one place (the reducer) instead of scattered across many handlers
  • Easier to test — the reducer is just a function
  • Easier to read the state machine — flip through cases
  • dispatch is stable, so you can pass it to children or down through context without useCallback

When To Use

SituationReach for
One value (a string, a boolean, a number)useState
Two or three related values, simple updatesuseState
Many fields that change togetheruseReducer
State machine (loading/error/data transitions)useReducer
Updates that involve multiple fields at onceuseReducer

Action Conventions

Most code uses an object with a type field. Extra payload goes on the object:

dispatch({ type: "set-name", name: "Ada" });
dispatch({ type: "add", item });
dispatch({ type: "clear" });

Some codebases prefer a payload field by convention; some prefer flat fields. Either is fine — be consistent.

Lazy Initialization

If computing the initial state is expensive, pass a third argument:

function init(initialUser) {
  return { user: initialUser, log: [] };
}

const [state, dispatch] = useReducer(reducer, savedUser, init);

init(savedUser) runs once on first render.

Pairing With Context

useReducer shines when paired with context — one provider exposes both state and dispatch to a whole subtree.

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

Any descendant can call dispatch({ type: "add", item }) — no prop drilling. (Plus, dispatch is stable, so no extra renders.)

Up Next

Both useState and useReducer are building blocks. The real power-up is wrapping them in your own custom hooks.

Custom Hooks →