Reducer pattern

2 snippets across 2 stacks — React, TypeScript

REReact

useReducer

RE · Additional Hooks
syntax
const [state, dispatch] = useReducer(reducer, initialState);
// reducer: (state, action) => newState
example
import { useReducer } from 'react';

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.payload),
      };
    case 'CLEAR':
      return { ...state, items: [] };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

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

  return (
    <div>
      <p>{cart.items.length} items</p>
      <button onClick={() =>
        dispatch({ type: 'ADD_ITEM', payload: { id: 1, name: 'Notebook' } })
      }>
        Add Notebook
      </button>
      <button onClick={() => dispatch({ type: 'CLEAR' })}>
        Clear Cart
      </button>
    </div>
  );
}

Note Prefer useReducer over useState when you have complex state logic with multiple sub-values, or when the next state depends on the previous state in varied ways. The reducer function must be pure -- no side effects.

TSTypeScript

Type-Safe Action / Reducer Pattern

TS · Common Patterns
syntax
type Action = { type: "name"; payload: Type } | ...;
function reducer(state: State, action: Action): State { ... }
example
interface AppState {
  count: number;
  message: string;
}

type AppAction =
  | { type: "increment"; amount: number }
  | { type: "decrement"; amount: number }
  | { type: "setMessage"; message: string }
  | { type: "reset" };

function reducer(state: AppState, action: AppAction): AppState {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + action.amount };
    case "decrement":
      return { ...state, count: state.count - action.amount };
    case "setMessage":
      return { ...state, message: action.message };
    case "reset":
      return { count: 0, message: "" };
  }
}
output
// Each case narrows the action type, giving access to case-specific fields

Note This is the standard pattern for Redux, useReducer, and state machines. The 'type' property serves as the discriminant. TypeScript narrows automatically in each case, so action.amount is only available in increment/decrement cases. Add a default never check for exhaustiveness.