React useReducer Hook

Manage complex state transitions and multi-step actions predictably using pure reducer functions with useReducer.

TL;DR

  1. Consolidate complex state transitions using the pure useReducer hook.
  2. Dispatch descriptive action objects specifying transition type and payload.
  3. Enforce compile-time safety across state actions using TypeScript discriminated unions.

Reducer Fundamentals

    Pure Reducer Function

    Calculate next state from current state and action object.

    function counterReducer(
      state: State,
      action: Action
    ): State {
      switch (action.type) {
        case 'inc': return { count: state.count + 1 };
        case 'dec': return { count: state.count - 1 };
        default: return state;
      }
    }
    Hook Invocation

    Initialize hook with reducer function and initial state.

    export function Counter() {
      const [state, dispatch] = useReducer(
        counterReducer,
        { count: 0 }
      );
      const inc = () => dispatch({ type: 'inc' });
      return <Button onClick={inc}>{state.count}</Button>;
    }
    Action with Payload

    Pass supplementary data alongside the action type key.

    dispatch({ type: 'setValue', payload: 42 });

TypeScript Action Typing

    Discriminated Union

    Enforce exact payload contracts per action type variant.

    type Action =
      | { type: 'reset' }
      | { type: 'setFilter'; payload: string }
      | { type: 'addItem'; payload: Item };
    Exhaustive Check Guard

    Ensure all action types are handled using TypeScript never.

    default: {
      const _exhaustive: never = action;
      return state;
    }
    Lazy Initialization

    Supply third initializer argument for costly computations.

    const [state, dispatch] = useReducer(
      reducer,
      initialArg,
      (init) => ({ items: loadFromDisk(init) })
    );

Complex State Workflows

    Form State Reducer

    Manage multi-field form changes and validation states.

    function formReducer(
      state: FormState,
      action: FormAction
    ) {
      switch (action.type) {
        case 'change':
          return { ...state, [action.field]: action.val };
        case 'reset':
          return initialFormState;
      }
    }
    Async Request State Machine

    Model loading, error, and data states explicitly.

    type State =
      | { status: 'idle' }
      | { status: 'loading' }
      | { status: 'success'; data: User }
      | { status: 'error'; error: Error };
    Undo and Redo Architecture

    Maintain past, present, and future state history arrays.

    interface HistoryState {
      past: State[];
      present: State;
      future: State[];
    }

Reducer Best Practices

    Keep Reducers Pure

    Reducers must never mutate state or make API requests.

    // BAD: Side effect in reducer
    case 'save':
      fetch('/api'); return state;
    Immutable State Updates

    Always return new objects with spread operator syntax.

    case 'update':
      return { ...state, count: action.payload };
    Dispatch Passing

    Pass dispatch down via context without causing re-renders.

    <DispatchContext.Provider value={dispatch}>

Tips

  1. Always keep reducer functions pure by computing next state without mutating arguments or triggering side-effects during calculation.
  2. Model state actions as descriptive event intentions like todoAdded rather than imperative setters like setTodo.

Warnings

  1. Never perform asynchronous operations or network fetches inside reducer functions because reducers must remain strictly synchronous.
  2. Avoid using useReducer for simple independent primitive values where useState offers cleaner code ergonomics.

In Practice

FAQ