mediumReact Native#130

Shopping Cart with Context

Prompt

Create a shopping cart using React Context. Requirements:

  1. CartContext with addItem, removeItem, updateQuantity, clearCart
  2. Each item has id, name, price, quantity
  3. Display cart total (price * quantity summed)
  4. +/- quantity controls that can remove item if qty reaches 0

Solution

const CartContext = createContext(null)

function CartProvider({ children }) {
  const [items, setItems] = useState([])
  const addItem = (product) => {
    setItems(prev => {
      const existing = prev.find(i => i.id === product.id)
      if (existing) return prev.map(i => i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i)
      return [...prev, { ...product, quantity: 1 }]
    })
  }
  const removeItem = (id) => setItems(prev => prev.filter(i => i.id !== id))
  const updateQuantity = (id, delta) => setItems(prev => prev.map(i => {
    if (i.id !== id) return i
    const qty = i.quantity + delta
    return qty <= 0 ? null : { ...i, quantity: qty }
  }).filter(Boolean))
  const clearCart = () => setItems([])
  const total = items.reduce((s, i) => s + i.price * i.quantity, 0)
  const value = { items, total, addItem, removeItem, updateQuantity, clearCart }
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>
}

function useCart() {
  const ctx = useContext(CartContext)
  if (!ctx) throw new Error('useCart must be used within CartProvider')
  return ctx
}
Mentor's take

A cart is the textbook case for Context: it's read from the catalog, the cart screen, and the checkout badge — prop-drilling it three navigator levels deep is the alternative, and that's worse. The shipping concern here is the module boundary: consumers get useCart() and a fixed API (addItem, removeItem, updateQuantity, clearCart); they never see setItems. Every invariant lives inside the provider.

Mechanics worth naming:

  • addItem is an upsert: find the existing line, increment its quantity with map-and-spread; otherwise append with quantity: 1. Duplicate line-items for the same product is the bug this prevents.
  • updateQuantity maps to null then filter(Boolean) — one pass that both updates and evicts when quantity hits zero, without mutating.
  • The guard hook: useCart throws when called outside the provider. createContext(null) plus that throw turns a silent undefined is not a function three components later into an immediate, named failure.
  • total is derived from items with reduce, never stored.

Trade-off to volunteer: value is a fresh object every provider render, so all consumers re-render on any cart change. Correct first; when a consumer gets expensive, wrap value in useMemo and, at scale, split state and actions into two contexts.

Red flag: putting setItems itself in the context value. The moment consumers can write raw state, the upsert and the qty-zero eviction stop being guarantees.

Say it: "The provider owns the invariants and exposes verbs, not setters — and useCart throws outside the provider so misuse fails loudly at the source."