Project 6

Project 6
Shopping Cart with Global State
A cart that needs to be readable and updatable from completely unrelated parts of the page — the real case for global state
Difficulty: Advanced Introduces: the Context API

Every previous project's state stayed within one small cluster of components, close enough together that lifting state up (Fundamentals Chapter 10) was a clean fit. A shopping cart is different: a product grid, a cart icon in the page header, and a full cart page all need to read or update the same cart — and they're nowhere near each other in the component tree. Lifting the cart all the way up to App and passing it down through every layer in between would mean prop drilling through components that have nothing to do with the cart at all. This is exactly the situation the Context API exists for, previewed here ahead of its full treatment in Intermediate Chapter 2.

Skills This Project Exercises
createContext / useContext Context Provider Derived values (totals, counts) Immutable array updates Avoiding prop drilling
The Context API — the minimum you need for this project
createContext() creates a context object; wrapping part of the tree in <CartContext.Provider value={...}> makes that value readable by any component nested inside it, no matter how deep, via useContext(CartContext) — with no props passed through the components in between at all.
const CartContext = createContext(null); function CartProvider({ children }) { const [items, setItems] = useState([]); // addToCart, removeFromCart, etc. defined here return ( <CartContext.Provider value={{ items, addToCart, removeFromCart }}> {children} </CartContext.Provider> ); } // anywhere deep inside <CartProvider>, with zero props passed down to reach it: const { items, addToCart } = useContext(CartContext);

Requirements

  • A product listing (hardcoded data is fine) with name, price, and an "Add to Cart" button on each.
  • A persistent cart indicator (e.g. in a header) showing the total number of items, visible regardless of which "page" is currently shown.
  • Adding the same product a second time increases its quantity in the cart, rather than creating a duplicate entry.
  • A cart view listing every item with its quantity, a per-item subtotal, and a running total for the whole cart.
  • A way to remove an item from the cart entirely, and a way to change an item's quantity directly.
  • Every piece of this (product grid, header badge, cart view) reads from the same single source of cart state — no two components should ever show conflicting cart data.

Suggested Component Breakdown

App └── CartProvider // owns cart state + addToCart/removeFromCart/updateQuantity ├── Header │ └── CartBadge // useContext — total item count ├── ProductList │ └── ProductCard (×N) // useContext — just needs addToCart └── CartPage └── CartItem (×N) // useContext — quantity, remove, subtotal

Notice that Header and ProductList are siblings, several layers removed from CartPage — exactly the layout where plain prop-passing would mean threading the cart through components that don't use it themselves. CartProvider wraps all three, and every consumer below it reaches the cart directly via useContext.

A Reasonable Build Order

  1. Build the static product grid and a non-functional cart page shell first, with no Context at all yet.
  2. Create CartContext and CartProvider, holding just the items array in useState, and wrap the whole app in it.
  3. Write addToCart: check whether the product is already in the array; if so, increase that entry's quantity, otherwise add a new entry with quantity 1 — both cases building a new array, never mutating the existing one.
  4. Wire ProductCard's button to call addToCart via useContext, and confirm the cart's contents are correct by logging them, before building any UI for the cart itself.
  5. Build CartBadge next — it only needs a derived total (sum of every item's quantity), read from the same context.
  6. Build out the full CartPage/CartItem views last, adding remove and quantity-update actions to the context's value alongside addToCart.
The duplicate-entry trap
The most common bug in a cart like this is addToCart always appending a new entry, so adding the same product three times produces three separate rows instead of one row with quantity 3. Always check first — find an existing entry with a matching product id, and only push a brand-new entry if none was found.
Stretch Goals
  • Persist the cart to localStorage, same pattern as Project 3, so it survives a refresh.
  • Replace the hand-rolled Context with a small state-management library — Zustand or Redux Toolkit, both covered properly in Advanced Chapter 1 — and compare how much boilerplate each removes versus plain Context.
  • Add quantity +/- stepper buttons instead of a raw number input.
  • Add a simple coupon-code field that applies a percentage discount to the total.
  • Add a checkout confirmation step that clears the cart afterward.
This project has no single solution file — the requirements above and your own judgment are the spec. Compare notes against the suggested component breakdown once you have something working, not before.