liveBook · Manning

useReducer

As explained in React in Depth

useReducer is a React hook that manages component state through a reducer function and dispatched actions.

useReducer keeps state transitions in a reducer and exposes dispatch for requesting them. It is a useful built-in option when state has several related operations: the transition logic lives in one place instead of being spread across component handlers. For application-wide data, context can expose the resulting state and actions without immediately adding a state-management library. section 6.1.5 section 8.9

A reorderable playlist

Model the list as State<T> = T[] and actions as a discriminated union: moveUp, moveDown, moveToTop, moveToBottom, and moveToPosition carry indices rather than song values. A generic reorder<T>(state, action) returns a State<T>; its moveUp case returns the original state for an invalid index and a newly reordered array otherwise. A custom hook calls useReducer(reorder, initial) and exposes the list plus named movement operations. The playlist component uses those operations in button handlers, while the same hook can reorder lists of other element types. section 6.1.5

Listing 6.2 The useReorderable hook
import { useReducer } from "react";
type State<T> = T[];
type Action =
  | { type: "moveUp"; index: number }
  | { type: "moveDown"; index: number }
  | { type: "moveToTop"; index: number }
  | { type: "moveToBottom"; index: number }
  | { type: "moveToPosition"; index: number; targetIndex: number };
function reorder<T>(state: State<T>, action: Action): State<T> {
  const newState: T[] = [...state];
  switch (action.type) {
    case "moveUp": {
      if (action.index <= 0 || action.index >= newState.length) {
        return state;
      }
      // Swap previous element with index
      [newState[action.index - 1], newState[action.index]] = [
        newState[action.index],
        newState[action.index - 1],
      ];
      return newState;
    }
    case "moveDown": {
      if (action.index < 0 || action.index >= newState.length - 1) {
        return state;
      }
      // Swap next element with index
      [newState[action.index], newState[action.index + 1]] = [
        newState[action.index + 1],
        newState[action.index],
      ];
      return newState;
    }
    case "moveToTop": {
      if (action.index <= 0 || action.index >= newState.length) {
        return state;
      }
      // Move index to top
      const itemToTop = newState.splice(action.index, 1)[0];
      newState.unshift(itemToTop);
      return newState;
    }
    case "moveToBottom": {
      if (action.index < 0 || action.index >= newState.length) {
        return state;
      }
      // Move index to bottom
      const itemToBottom = newState.splice(action.index, 1)[0];
      newState.push(itemToBottom);
      return newState;
    }
    default:
      return state;
  }
}
export function useReorderable<T>(initial: State<T>) {
  const [state, dispatch] = useReducer<
    Reducer<State<T>, Action>
  >(reorder, initial);
  return {
    list: state,
    moveUp: (index: number) =>     #1
      dispatch({ type: "moveUp", index }),   #1
    moveDown: (index: number) =>    #1
      dispatch({ type: "moveDown", index }),   #1
    moveToTop: (index: number) =>   #1
      dispatch({ type: "moveToTop", index }),  #1
    moveToBottom: (index: number) =>   #1
      dispatch({ type: "moveToBottom", index }),    #1
  };
}

The full reducer and custom hook show how typed actions become reusable list operations, including index checks and array updates. section 6.1.5

TypeScript generally follows the reducer’s state and action types. For generic reducers, older React typings can lose the element type at useReducer; explicitly supplying Reducer<State<T>, Action> restores it. React 19 fixes this inference issue, so that hint is no longer needed. Keep the version-specific workaround only where the project requires it. section 6.1.5

For larger shared state, pair useReducer with context and consider useContextSelector to limit re-renders. useReduction can reduce reducer boilerplate, while Immer eases immutable updates. RTK, Zustand, and XState are alternatives, not automatic upgrades: choose according to the state and workflow rather than adding a library by default. section 8.4 section 8.9

How React in Depth covers it

How deep it goes
Works through a generic reducer, typed actions, a custom hook, and a playlist; distinguishes older generic inference from React 19.
Examples
Uses a reorderable playlist with index-based movement actions and a goal-tracking application compared across state-management approaches.
What it assumes
Assumes React and TypeScript familiarity and moves directly into reducers, generic typing, and component integration.
What you can do after
Build it from scratch as a typed, reusable list-state hook, or apply it with context before reaching for an external store.
How it connects
Connects reducers and discriminated unions to TypeScript generics, context, useContextSelector, useReduction, and Immer.

Also explained in

Compare how 3 books explain useReducer →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of React in Depth. The section links point to the parts of the book it draws on.