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

Context + useReducer Together

A provider component that uses useReducer for its state and exposes the state and dispatch-based functions through context.

react.devuseReducer

Notes

This combines Entries 4 and 5 into a lightweight global store, the pattern Redux formalizes:

  • Context solves distribution (no prop drilling).
  • useReducer solves update logic (centralized, testable transitions).

Only the provider changes. Consumers keep using useCart() with the same API, so switching from useState to useReducer is invisible to them.

What / Why / When

What

Shared state with centralized transitions.

Why

The reducer can move to its own file and be tested alone, while consumers stay simple.

When

Context state with several kinds of updates. For large apps with many slices of state, move on to Redux Toolkit (later chapter).

Use case

The cart from Entry 4, rewritten with a reducer.

Code compare

React
// store/cart-context.tsximport { createContext, useContext, useReducer, type ReactNode } from "react";​export type CartItem = { id: string; name: string; price: number; quantity: number };​type CartAction =  | { type: "ADD_ITEM"; payload: Omit<CartItem, "quantity"> }  | { type: "UPDATE_ITEM_QUANTITY"; payload: { id: string; amount: number } };​function cartReducer(state: CartItem[], action: CartAction): CartItem[] {  switch (action.type) {    case "ADD_ITEM": {      const existing = state.find((item) => item.id === action.payload.id);      if (existing) {        return state.map((item) =>          item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item,        );      }      return [...state, { ...action.payload, quantity: 1 }];    }​    case "UPDATE_ITEM_QUANTITY":      return state        .map((item) =>          item.id === action.payload.id            ? { ...item, quantity: item.quantity + action.payload.amount }            : item,        )        .filter((item) => item.quantity > 0);  }}​type CartContextValue = {  items: CartItem[];  addItem: (product: Omit<CartItem, "quantity">) => void;  updateItemQuantity: (id: string, amount: number) => void;};​const CartContext = createContext<CartContextValue | null>(null);​export function CartContextProvider({ children }: { children: ReactNode }) {  const [items, dispatch] = useReducer(cartReducer, []);​  const ctxValue: CartContextValue = {    items,    addItem: (product) => dispatch({ type: "ADD_ITEM", payload: product }),    updateItemQuantity: (id, amount) =>      dispatch({ type: "UPDATE_ITEM_QUANTITY", payload: { id, amount } }),  };​  return <CartContext value={ctxValue}>{children}</CartContext>;}​export function useCart() {  const ctx = useContext(CartContext);  if (!ctx) throw new Error("useCart must be used inside <CartContextProvider>");  return ctx;}

No change is needed: the CartService from Entry 4 already combines distribution (dependency injection) and centralized updates (service methods). This is why Angular developers rarely reach for a reducer in small or medium apps.

No Angular code for this example.

Differences and gotchas

  • dispatch has a stable identity across renders, so passing it (or functions built on it) through context is cheap.
  • Angular's signal service is roughly React's Context + useReducer + useMemo in one class, with fine-grained updates instead of consumer re-renders.

Quick reference

ReactAngular
Context (distribution)Dependency injection
useReducer (update logic)Service methods + signal.update()
Context + useReducerOne signal-based service
Redux Toolkit (large apps)NgRx Store / SignalStore