SyntaxStudy
Sign Up
React Context with useReducer for Complex State
React Beginner 1 min read

Context with useReducer for Complex State

Combining useReducer with Context gives you a lightweight, built-in alternative to external state management for moderately complex global state. useReducer centralises all state transitions into a pure function, making the logic easy to test in isolation. Dispatching actions from anywhere in the tree via Context removes the need for callbacks threaded through many layers of props. The standard pattern is to expose two separate contexts from the same Provider: one for the state value and one for the dispatch function. Because dispatch never changes identity, components that only dispatch actions will not re-render when state changes, which keeps the tree efficient. This pattern scales well up to the point where your reducer becomes hard to reason about or you need middleware, time-travel debugging, or fine-grained subscriptions. At that point, adopting a dedicated library is the right call, but the Context plus useReducer combination handles a surprising amount of real-world complexity cleanly.
Example
import { createContext, useContext, useReducer } from 'react';

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

// ── Two contexts: state + dispatch ─────────────────────────────────────────
const CartStateContext    = createContext(null);
const CartDispatchContext = createContext(null);

export function CartProvider({ children }) {
    const [state, dispatch] = useReducer(cartReducer, { items: [] });
    return (
        <CartStateContext.Provider value={state}>
            <CartDispatchContext.Provider value={dispatch}>
                {children}
            </CartDispatchContext.Provider>
        </CartStateContext.Provider>
    );
}

export const useCartState    = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);

// ── Consumer ───────────────────────────────────────────────────────────────
function CartBadge() {
    const { items } = useCartState(); // re-renders on state change
    return <span>{items.length}</span>;
}

function AddToCartButton({ product }) {
    const dispatch = useCartDispatch(); // stable – never re-renders
    return (
        <button onClick={() => dispatch({ type: 'ADD_ITEM', item: product })}>
            Add to cart
        </button>
    );
}