Wire the Cart into the App

The reducer, context, and provider are ready. Now let’s connect them to the UI: create a useCart hook for components to consume the cart, wrap the app in the provider, and add a badge that shows the item count.

Create the useCart hook

Create src/hooks/use-cart.ts:

import { useContext } from "react";
import { CartContext } from "@/context/cart-context";

export function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error("useCart must be used within a CartProvider");
  }
  return context;
}

This is a thin wrapper around useContext with a runtime guard. If a component calls useCart() without a CartProvider ancestor, it throws a clear error instead of returning null. So instead of a confusing “cannot read property of null” error later on, you get a message that says what is wrong. This pattern is common for React contexts.

The header needs a cart icon with a badge showing the item count. This component calls useCart(), so it must be rendered inside the CartProvider. We can not put the hook call directly in RootLayout because the provider wraps the layout’s children, not the layout itself. Extracting it into its own component solves this.

Create src/components/cart-link.tsx:

import { Link } from "@tanstack/react-router";
import { ShoppingCart } from "lucide-react";
import { useCart } from "@/hooks/use-cart";
import { Badge } from "@/components/ui/badge";

export function CartLink() {
  const { totalItems } = useCart();

  return (
    <Link
      to="/cart"
      className="relative text-muted-foreground hover:text-foreground [&.active]:text-foreground"
    >
      <ShoppingCart className="h-5 w-5" />
      {totalItems > 0 && (
        <Badge className="absolute -top-2 -right-3 h-5 min-w-5 justify-center px-1 text-xs">
          {totalItems}
        </Badge>
      )}
    </Link>
  );
}

The badge only appears when totalItems > 0.

Wire the provider into the root layout

Update src/routes/__root.tsx to wrap the app with CartProvider and use CartLink:

  import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
+ import { CartProvider } from "@/providers/cart-provider";
+ import { CartLink } from "@/components/cart-link";

  export const Route = createRootRoute({
    component: RootLayout,
  });

  function RootLayout() {
    return (
+     <CartProvider>
        <div className="min-h-screen bg-background text-foreground">
          <header className="border-b">
            <nav className="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
              <Link to="/" className="text-xl font-bold">
                eStore
              </Link>
              <div className="flex items-center gap-4">
                <Link
                  to="/"
                  className="text-sm text-muted-foreground hover:text-foreground [&.active]:text-foreground"
                >
                  Products
                </Link>
-               <Link
-                 to="/cart"
-                 className="text-sm text-muted-foreground hover:text-foreground [&.active]:text-foreground"
-               >
-                 <ShoppingCart className="h-5 w-5" />
-               </Link>
+               <CartLink />
              </div>
            </nav>
          </header>
          <main className="mx-auto max-w-5xl px-4 py-8">
            <Outlet />
          </main>
        </div>
+     </CartProvider>
    );
  }

CartProvider wraps the entire layout, so the cart state is available to every page component rendered inside <Outlet /> and to CartLink in the header.

The cart is not wired to any buttons yet, so you will not see the badge until the next section.

Checkpoint: Commit your progress.

git add .
git commit -m "shopping-cart-08: Wire cart provider and hook into the app"
git push