Cart Reducer

The cart needs to be accessible from multiple places: the product detail page (to add items), the cart page (to display and manage items), and the header (to show a badge count). With what we know so far, we would have to lift the cart state up to the root layout and pass it down through props to every component that needs it. This is called prop drilling, and it quickly becomes difficult to maintain.

React Context solves this. It lets you create a value that any component in the tree can access directly, without passing it through intermediate components. Over the next three sections, we will build the cart using Context. First, let’s define the state shape and the reducer that manages it.

Define types and the reducer

Create src/reducers/cart-reducer.ts:

import type { Product } from "@/api/types";

export type CartItem = {
  product: Product;
  quantity: number;
};

type CartState = {
  items: CartItem[];
};

type CartAction =
  | { type: "added"; product: Product }
  | { type: "removed"; productId: number }
  | { type: "quantity_updated"; productId: number; quantity: number }
  | { type: "cleared" };

export function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case "added": {
      const existing = state.items.find(
        (item) => item.product.id === action.product.id,
      );
      if (existing) {
        return {
          items: state.items.map((item) =>
            item.product.id === action.product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item,
          ),
        };
      }
      return {
        items: [...state.items, { product: action.product, quantity: 1 }],
      };
    }
    case "removed":
      return {
        items: state.items.filter(
          (item) => item.product.id !== action.productId,
        ),
      };
    case "quantity_updated":
      if (action.quantity <= 0) {
        return {
          items: state.items.filter(
            (item) => item.product.id !== action.productId,
          ),
        };
      }
      return {
        items: state.items.map((item) =>
          item.product.id === action.productId
            ? { ...item, quantity: action.quantity }
            : item,
        ),
      };
    case "cleared":
      return { items: [] };
  }
}

This is the same useReducer pattern from the KudoBoard chapter, extracted into its own file:

  • CartItem — a product with a quantity
  • CartAction — the four things that can happen to the cart: add a product, remove one, update a quantity, or clear everything
  • cartReducer — handles each action. The "added" case checks if the product already exists and bumps the quantity instead of adding a duplicate. The "quantity_updated" case removes the item if the quantity drops to zero or below.

The reducer is a pure function: no React, no hooks, no side effects. It takes state and an action, and returns new state. We will wire it into React with useReducer in the next section.

Checkpoint: Commit your progress.

git add .
git commit -m "shopping-cart-06: Cart reducer and types"
git push