Cart Context and Provider

With the reducer in place, we can now build the Context layer. This involves two pieces: a Context object (the value components read) and a Provider component (the component that holds the state and makes it available).

Create the context

Create src/context/cart-context.tsx:

import { createContext } from "react";
import type { Product } from "@/api/types";
import type { CartItem } from "@/reducers/cart-reducer";

export type CartContextValue = {
  items: CartItem[];
  addToCart: (product: Product) => void;
  removeFromCart: (productId: number) => void;
  updateQuantity: (productId: number, quantity: number) => void;
  clearCart: () => void;
  totalItems: number;
  totalPrice: number;
};

export const CartContext = createContext<CartContextValue | null>(null);

CartContextValue describes everything the cart makes available to consumers: the list of items, four action functions, and two computed totals. The context starts as null. Components that try to use it outside a provider will get null, which we will catch with a guard in the hook later.

Create the provider

Create src/providers/cart-provider.tsx:

import { useReducer } from "react";
import type { ReactNode } from "react";
import { cartReducer } from "@/reducers/cart-reducer";
import { CartContext } from "@/context/cart-context";
import type { Product } from "@/api/types";

export function CartProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  const addToCart = (product: Product) => {
    dispatch({ type: "added", product });
  };

  const removeFromCart = (productId: number) => {
    dispatch({ type: "removed", productId });
  };

  const updateQuantity = (productId: number, quantity: number) => {
    dispatch({ type: "quantity_updated", productId, quantity });
  };

  const clearCart = () => {
    dispatch({ type: "cleared" });
  };

  const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);

  const totalPrice = state.items.reduce(
    (sum, item) => sum + item.product.price * item.quantity,
    0,
  );

  return (
    <CartContext
      value={{
        items: state.items,
        addToCart,
        removeFromCart,
        updateQuantity,
        clearCart,
        totalItems,
        totalPrice,
      }}
    >
      {children}
    </CartContext>
  );
}

Here is what the provider does:

  1. useReducer manages the cart state using the reducer from the previous section
  2. Four convenience functions wrap dispatch so consumers do not need to know about action types
  3. totalItems and totalPrice are computed from the current state on every render
  4. All of this is passed through CartContext to any descendant component

Notice that the reducer, the context, and the provider are each in their own file. The reducer manages the state logic, the context defines what is shared, and the provider connects the reducer to the context. Keeping them in separate files makes each one easier to understand, test, and reuse.

Checkpoint: Commit your progress.

git add .
git commit -m "shopping-cart-07: Cart context and provider"
git push