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.
Notes
This combines Entries 4 and 5 into a lightweight global store, the pattern Redux formalizes:
- Context solves distribution (no prop drilling).
useReducersolves 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
// 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
dispatchhas 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+useMemoin one class, with fine-grained updates instead of consumer re-renders.
Quick reference
| React | Angular |
|---|---|
| Context (distribution) | Dependency injection |
useReducer (update logic) | Service methods + signal.update() |
Context + useReducer | One signal-based service |
| Redux Toolkit (large apps) | NgRx Store / SignalStore |