State Management

useReducer

Manages complex state logic with a reducer function. Preferred over useState when state transitions depend on the previous state or involve multiple sub-values.

Signature

TypeScript
const [state, dispatch] = useReducer<R>(reducer: R, initialArg: ReducerState<R>, init?: (arg: ReducerState<R>) => ReducerState<R>)

Parameters

ParameterTypeDescription
reducer(state: S, action: A) => SA pure function that takes current state and an action, then returns the new state.
initialArgSThe initial state value. If init is provided, the initial state is set to init(initialArg).
init(arg: S) => SOptional lazy initializer function. Called with initialArg to compute the initial state.

Return Value

A tuple of [state, dispatch]. state is the current state value. dispatch is a function that accepts an action and triggers the reducer to compute the next state.

Examples

Todo List Reducer
import { useReducer } from 'react';

type Todo = { id: number; text: string; done: boolean };
type Action =
  | { type: 'add'; text: string }
  | { type: 'toggle'; id: number }
  | { type: 'delete'; id: number };

function todoReducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case 'add':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'toggle':
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'delete':
      return state.filter(t => t.id !== action.id);
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  return (
    <div>
      <button onClick={() => dispatch({ type: 'add', text: 'New task' })}>
        Add
      </button>
      {todos.map(t => (
        <div key={t.id}>
          <span style={{ textDecoration: t.done ? 'line-through' : 'none' }}>
            {t.text}
          </span>
          <button onClick={() => dispatch({ type: 'toggle', id: t.id })}>✓</button>
          <button onClick={() => dispatch({ type: 'delete', id: t.id })}>×</button>
        </div>
      ))}
    </div>
  );
}
Form State Machine
import { useReducer } from 'react';

type FormState = {
  status: 'idle' | 'submitting' | 'success' | 'error';
  values: { email: string; message: string };
  error: string | null;
};

type FormAction =
  | { type: 'field'; field: string; value: string }
  | { type: 'submit' }
  | { type: 'success' }
  | { type: 'error'; error: string }
  | { type: 'reset' };

const initial: FormState = {
  status: 'idle',
  values: { email: '', message: '' },
  error: null,
};

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'field':
      return { ...state, values: { ...state.values, [action.field]: action.value } };
    case 'submit':
      return { ...state, status: 'submitting', error: null };
    case 'success':
      return { ...initial, status: 'success' };
    case 'error':
      return { ...state, status: 'error', error: action.error };
    case 'reset':
      return initial;
  }
}
Lazy Initialization
import { useReducer } from 'react';

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

type CartAction =
  | { type: 'add'; id: string; price: number }
  | { type: 'remove'; id: string }
  | { type: 'clear' };

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'add': {
      const items = [...state.items, { id: action.id, qty: 1, price: action.price }];
      return { items, total: items.reduce((s, i) => s + i.price * i.qty, 0) };
    }
    case 'remove': {
      const items = state.items.filter(i => i.id !== action.id);
      return { items, total: items.reduce((s, i) => s + i.price * i.qty, 0) };
    }
    case 'clear':
      return { items: [], total: 0 };
  }
}

function Cart({ savedCart }: { savedCart: string }) {
  const [state, dispatch] = useReducer(
    cartReducer,
    savedCart,
    (saved) => JSON.parse(saved) as CartState,
  );
  return <p>Cart total: ${state.total.toFixed(2)}</p>;
}

Common Pitfalls

!

Mutating state inside the reducer instead of returning a new object — reducers must be pure functions.

!

Not handling all action types, which can cause the reducer to return undefined.

!

Overcomplicating simple state — useState is often enough for a single value or a boolean toggle.

!

Placing side effects (API calls, localStorage writes) inside the reducer — use useEffect for side effects.

Understanding useReducer

useReducer is React's built-in alternative to useState for managing complex state logic. Inspired by the Redux pattern, it centralizes state transitions in a single reducer function, making it easier to reason about how state changes in response to different actions. This is particularly valuable when multiple state values are interdependent or when the next state depends on the previous one.

The reducer function must be pure — given the same state and action, it should always return the same new state. It should not perform side effects like API calls, write to localStorage, or modify external variables. This purity makes reducers highly testable: you can unit test every state transition by simply calling the function with known inputs and asserting the output.

The dispatch function returned by useReducer is stable across re-renders, meaning it never changes identity. This makes it safe to pass to child components or include in dependency arrays without causing unnecessary re-renders. When combined with useContext, the dispatch function can be provided to deeply nested components, effectively creating a lightweight state management system without external libraries.

Lazy initialization via the third argument is useful when the initial state requires parsing JSON, reading from storage, or any non-trivial computation. React calls the init function only on the first render, similar to the lazy initializer in useState. The pattern of separating the initial argument from the init function also enables a clean "reset" action that can recompute initial state from the original argument.

Related Hooks

More State Management Hooks

Explore All React Hooks

Browse our complete reference of 19 React hooks with signatures, examples, pitfalls, and in-depth explanations.