Project 6
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.
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.
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
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
- Build the static product grid and a non-functional cart page shell first, with no Context at all yet.
- Create
CartContextandCartProvider, holding just theitemsarray inuseState, and wrap the whole app in it. - 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. - Wire
ProductCard's button to calladdToCartviauseContext, and confirm the cart's contents are correct by logging them, before building any UI for the cart itself. - Build
CartBadgenext — it only needs a derived total (sum of every item's quantity), read from the same context. - Build out the full
CartPage/CartItemviews last, adding remove and quantity-update actions to the context's value alongsideaddToCart.
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.
- 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.