React's Context API & useReducer — Advanced State ManagementEntry 5 of 7

useReducer: Actions, Payloads, and Pure Reducers

A hook that manages state through a reducer function: components dispatch actions, and the reducer computes the next state.

react.devuseReducer

Notes

const [state, dispatch] = useReducer(reducer, initialState);
  • Reducer: a pure function (state, action) => newState. No side effects, no mutation; it always returns a new state.
  • Action: an object describing what happened, with a type and an optional payload carrying the data.
  • Dispatch: the function components call to send an action. The UI never changes state directly; it describes the event, and the reducer decides the result.

In TypeScript, model actions as a discriminated union. Each case then knows its exact payload type, and the compiler flags unknown action types.

What / Why / When

What

Centralized, event-driven state updates.

Why

All state transitions live in one testable function, separate from the UI.

When

State has several related values or many ways to change, or the next state depends on the previous one in non-trivial ways. For a simple toggle or single value, useState is enough.

Use case

A player manager with add, update, remove, and reset actions.

Code compare

React
// players-reducer.tsexport type Player = { id: string; name: string };​type PlayersState = { players: Player[] };​type PlayersAction =  | { type: "add-player"; payload: Player }  | { type: "update-player"; payload: Player }  | { type: "remove-player"; payload: { id: string } }  | { type: "reset" };​export const initialState: PlayersState = { players: [] };​export function playersReducer(state: PlayersState, action: PlayersAction): PlayersState {  switch (action.type) {    case "add-player":      return { ...state, players: [...state.players, action.payload] };​    case "update-player":      return {        ...state,        players: state.players.map((p) =>          p.id === action.payload.id ? { ...p, name: action.payload.name } : p,        ),      };​    case "remove-player":      return { ...state, players: state.players.filter((p) => p.id !== action.payload.id) };​    case "reset":      return initialState;  }}

Angular has no built-in reducer. The idiomatic equivalent is a service whose methods play the role of actions:

Angular
// player.store.tsimport { Injectable, signal } from "@angular/core";​export type Player = { id: string; name: string };​@Injectable({ providedIn: "root" })export class PlayerStore {  private readonly _players = signal<Player[]>([]);  readonly players = this._players.asReadonly();​  addPlayer(name: string) {    this._players.update((list) => [...list, { id: crypto.randomUUID(), name }]);  }​  updatePlayer(id: string, name: string) {    this._players.update((list) => list.map((p) => (p.id === id ? { ...p, name } : p)));  }​  removePlayer(id: string) {    this._players.update((list) => list.filter((p) => p.id !== id));  }​  reset() {    this._players.set([]);  }}

Differences and gotchas

  • dispatch({ type: 'remove-player', payload: { id } }) in React becomes a method call store.removePlayer(id) in Angular. Same idea, less ceremony.
  • React's reducer is a standalone pure function, easy to unit-test without rendering. In Angular, test the service's methods instead.
  • For a reducer-style or Redux-style store in Angular, NgRx provides signalState/SignalStore and the classic NgRx Store.
  • With a discriminated union, TypeScript checks the switch for you. In plain JavaScript, add default: throw new Error('Unknown action').

Quick reference

ReactAngular
useReducer(reducer, initialState)Service with a private signal()
dispatch({ type, payload })store.method(args)
Reducer caseService method using update()
Pure reducer (unit-testable)Service methods (unit-testable)