# useReducer

## 1. The Basic API

`useReducer` manages state via a **pure function** that takes the current state and an "action" and returns the next state, instead of many separate setter calls:

```tsx
type CartAction =
  | { type: "add"; payload: { id: string; price: number } }
  | { type: "remove"; payload: { id: string } }
  | { type: "clear" };

interface CartState { items: { id: string; price: number }[] }

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case "add":
      return { items: [...state.items, action.payload] };
    case "remove":
      return { items: state.items.filter((i) => i.id !== action.payload.id) };
    case "clear":
      return { items: [] };
  }
}

function Cart() {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  return (
    <div>
      <button onClick={() => dispatch({ type: "add", payload: { id: "sku1", price: 9.99 } })}>
        Add item
      </button>
      <button onClick={() => dispatch({ type: "clear" })}>Clear cart</button>
      <p>Total items: {state.items.length}</p>
    </div>
  );
}
```

`dispatch(action)` never mutates `state` directly — it hands `action` to your reducer, which computes and returns the entire next state.

## 2. Reducers Must Be Pure

A reducer receives `(state, action)` and must return a new state derived only from those two arguments — no `fetch`, no `Math.random()`, no mutation of the incoming `state`, no side effects of any kind. Given the same `state` and `action`, it must always return the same result.

```tsx
// WRONG — mutates state in place
function badReducer(state: CartState, action: CartAction): CartState {
  if (action.type === "add") {
    state.items.push(action.payload); // mutation — breaks reference equality checks
    return state; // same reference — React may not detect a change at all
  }
  return state;
}

// RIGHT — returns a new object/array every time something changes
function goodReducer(state: CartState, action: CartAction): CartState {
  if (action.type === "add") {
    return { items: [...state.items, action.payload] }; // new array, new object
  }
  return state;
}
```

Side effects (API calls, logging, navigation) belong in an event handler or a `useEffect` that runs in response to the resulting state — never inside the reducer itself.

## 3. Lazy Initialization with the Third Argument

`useReducer` accepts a third argument, `init`, mirroring `useState`'s lazy initializer — `init(initialArg)` runs exactly once, on mount, to compute the real starting state:

```tsx
interface FormState { fields: Record<string, string>; touched: Set<string> }

function init(defaultFields: Record<string, string>): FormState {
  return { fields: { ...defaultFields }, touched: new Set() }; // runs once, on mount only
}

function Form({ defaults }: { defaults: Record<string, string> }) {
  const [state, dispatch] = useReducer(formReducer, defaults, init);
  // ...
}
```

This is useful when building the initial state is non-trivial (parsing, cloning, deriving from props) and shouldn't be recomputed on every render.

## 4. When to Reach for useReducer Instead of useState

- **Multiple fields update together as one atomic transition** — one `dispatch({ type: "submit" })` can update several fields consistently, instead of coordinating several `setState` calls that could each trigger an intermediate, inconsistent render.
- **The next state depends on the previous state in a non-trivial way** — complex branching logic reads far more clearly as a `switch` in one pure function than scattered across several `setX(prev => ...)` calls.
- **You want transition logic to be independently testable** — a reducer is a plain function; test it directly with `expect(reducer(state, action)).toEqual(...)`, no rendering required.
- **You want to pass `dispatch` down instead of many individual setters** — `dispatch`'s reference is stable across renders (see Under the Hood), so passing it through props or context never risks causing extra re-renders the way passing a fresh inline callback would.

## 5. Combining useReducer with useContext

A `[state, dispatch]` pair from `useReducer`, exposed through `useContext`, gives a lightweight, centralized store without any external library — a common lightweight alternative to Redux/Zustand for small-to-medium apps:

```tsx
const StoreContext = createContext<{ state: CartState; dispatch: React.Dispatch<CartAction> } | undefined>(undefined);

function StoreProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  const value = useMemo(() => ({ state, dispatch }), [state]); // dispatch is stable, state isn't
  return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}

function useStore() {
  const ctx = useContext(StoreContext);
  if (!ctx) throw new Error("useStore must be used within a StoreProvider");
  return ctx;
}
```

Any component can now `const { state, dispatch } = useStore()` and dispatch actions without prop drilling. This pattern — and its limitations at larger scale — is covered in depth in the State Management Patterns topic.

![dispatch(action) enqueues the action on the hook's pending queue; the next render walks the queue, calling the reducer once per queued action and threading results forward](diagrams/01-reducer-dispatch-queue.svg)

## Under the Hood

React does not attach state to the component *function* — a function component is just a function, called fresh every render with no persistent `this` and no memory of its own. Hook data has to live somewhere else: it lives on the **fiber**, React's internal object representing one component instance in the tree.

Each fiber holds a `memoizedState` pointer to the head of a **singly linked list of hook objects**, one node per hook call, built in the exact order those hooks are called. Every render, React walks this list from the head, one node per hook call, in lockstep with your function body executing top to bottom. Matching is done purely by **call order** — not by variable name, not by any key. This is the entire reason the Rules of Hooks forbid conditional hook calls: wrap a hook in an `if` and, on some render, every hook call after it shifts one slot in the walk — silently reading and writing the wrong hook's stored data.

A `useReducer` hook node stores the same two things a `useState` node does: a `memoizedState` value slot and a queue of pending updates — because `useState` is actually implemented internally on top of this exact same reducer mechanism (a hidden built-in "basic state reducer," which just replaces the value or applies an updater function, is what `useState` uses under the hood; `useReducer` simply lets you supply your own reducer function instead of that built-in one). Calling `dispatch(action)` does not run your reducer synchronously against the current `memoizedState` — it enqueues the `action` object onto the hook's pending queue and schedules a re-render, exactly like calling a `useState` setter enqueues an update. On the next render, before your component body runs, React walks that queue front to back and calls **your** reducer function once per queued action, threading the result of each call into the next as the running state, finally writing the last result into `memoizedState`. `dispatch` itself is a stable function reference for the lifetime of the component — it's closed over the fiber and the hook's queue, not recreated on each render — which is why passing `dispatch` down through props or context never needs `useCallback` to avoid causing extra re-renders the way an inline arrow function prop typically would.

## Common Pitfalls

1. **Mutating state inside the reducer** instead of returning a new object/array — breaks reference-equality checks React relies on and can silently produce stale renders.
2. **Putting side effects inside the reducer** (API calls, logging, timers) — reducers must stay pure; trigger side effects from the event handler that dispatches, or from a `useEffect` reacting to the resulting state.
3. **Forgetting a `default` case (or an exhaustiveness check)** in the action-type switch — an unhandled action type silently falls through and returns state unchanged with no warning.
4. **Reaching for `useReducer` on genuinely simple, single-value state** — a lone counter or toggle is clearer with `useState`; `useReducer`'s value shows up on multi-field, interdependent state transitions.

## Quick Reference

```
const [state, dispatch] = useReducer(reducer, initialState)          → basic usage
const [state, dispatch] = useReducer(reducer, initialArg, init)      → lazy init, init() runs once
dispatch({ type: "..." , payload: ... })                              → trigger a transition
function reducer(state, action) { switch (action.type) { ... } }      → must be pure, return new state
dispatch reference is STABLE across renders                           → safe to pass via context/props
```
