React Context

A Cart Context

Context shares state with a subtree without passing props. In the App Router, the provider is a Client Component placed in a layout.

// components/cart/CartProvider.tsx
'use client';

import { createContext, useContext, useState } from 'react';

type CartItem = { productId: number; qty: number };
type CartCtx = { items: CartItem[]; add: (id: number) => void; count: number };

const CartContext = createContext<CartCtx | null>(null);

export function CartProvider({ children, initial }: { children: React.ReactNode; initial: CartItem[] }) {
  const [items, setItems] = useState(initial);
  const add = (productId: number) =>
    setItems((prev) => {
      const found = prev.find((i) => i.productId === productId);
      return found
        ? prev.map((i) => (i.productId === productId ? { ...i, qty: i.qty + 1 } : i))
        : [...prev, { productId, qty: 1 }];
    });
  const count = items.reduce((n, i) => n + i.qty, 0);
  return <CartContext.Provider value={{ items, add, count }}>{children}</CartContext.Provider>;
}

export function useCart() {
  const ctx = useContext(CartContext);
  if (!ctx) throw new Error('useCart must be used inside <CartProvider>');
  return ctx;
}

Using It

// app/layout.tsx (server): load the cart, pass it as initial state
// <CartProvider initial={await getCartItems()}>{children}</CartProvider>

// components/CartBadge.tsx
'use client';
import { useCart } from './cart/CartProvider';

export default function CartBadge() {
  const { count } = useCart();
  return <span className="badge">{count}</span>;
}

Note: Context re-renders every consumer when the value changes. Fine for theme, user and small carts; for frequent updates prefer Zustand with selectors.