Building Shared Toast and Cart Providers

Building Toast Notifications

Welcome back. In the previous lesson, Building the Product Detail Hook, you created a reusable useProduct() hook, connected a dynamic product route, and completed the browse flow from the product grid into a dedicated detail page. At that point, the storefront could already load collections and individual products in a clean, reusable way.

This lesson adds another important piece of real storefront behavior: global feedback and cart-aware shell state. When users add items, update quantities, or place an order, the UI should respond immediately with lightweight notifications instead of forcing users to guess whether something happened. At the same time, the app needs a shared cart layer that can restore the shopper’s cart between page reloads and expose simple values, like the current item count, to components such as the navbar.

The big idea here is the same pattern you have been building throughout the course: shared infrastructure should simplify pages, not complicate them. Toasts should be managed by one provider. Cart requests should live in a typed API helper layer. Cart restoration and orchestration should live in a hook/provider, not inside pages. And shared UI like the navbar should consume prepared values instead of understanding storage or request details itself.

Building a Global Toast System

The toast system lives in src/components/ui/Toast.tsx. This file is not responsible for representing one toast in isolation. Its real job is to act as a provider for the entire temporary notification system.

That means it needs to do three things well:

  • keep track of the current list of active toasts
  • expose simple helper functions like success() and error()
  • render the fixed toast stack above the page without taking up normal layout space

We will look at the file in three parts so the responsibilities stay clear.

Defining the Toast Types and Context

The top of src/components/ui/Toast.tsx defines the toast model and the public context API. This is where the file describes what a toast looks like and what capabilities the rest of the app can consume.

'use client';

import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';

export type Toast = { id: number; message: string; type: 'success' | 'error' | 'info' };

type ToastContextValue = {
  success: (message: string) => void;
  error: (message: string) => void;
  info: (message: string) => void;
};

const ToastContext = createContext<ToastContextValue | null>(null);
  • The 'use client' directive is required because this file uses React state and context, both of which belong on the client side. A toast system is inherently interactive and time-based, so it must run in the client component world.

  • The Toast type describes one notification object. Each toast needs an id so React can track it reliably in a list, a message for the visible text, and a type so the UI can style success, error, and informational notifications differently.

  • ToastContextValue intentionally exposes a very small public API. Consumers do not need to know anything about IDs, state arrays, or timers. They only need expressive intent-based helpers like success(message), error(message), and info(message).

  • createContext<ToastContextValue | null>(null) creates the shared context that will later be filled by the provider. Starting with null is a common safety pattern because it makes it possible for the custom hook to detect misuse outside the provider tree.

This top section is small, but it establishes an important design principle: the provider owns the mechanics, while the rest of the app gets a simple interface.

Managing Toast State and Push Logic

The next part of the file defines the provider state and the shared push() helper. This is the core logic of the toast system, because it is responsible for creating toasts, appending them to state, and scheduling their automatic removal.

export function ToasterProvider({ children }: { children: ReactNode }) {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const push = useCallback((message: string, type: Toast['type']) => {
    const id = Date.now() + Math.random();
    setToasts((current) => [...current, { id, message, type }]);
    setTimeout(() => {
      setToasts((current) => current.filter((toast) => toast.id !== id));
    }, 2500);
  }, []);

  const value = useMemo<ToastContextValue>(
    () => ({
      success: (message) => push(message, 'success'),
      error: (message) => push(message, 'error'),
      info: (message) => push(message, 'info'),
    }),
    [push]
  );
  • useState<Toast[]>([]) stores the list of active toasts. This is the provider-level state for the whole notification system, which is why the file should be thought of as a toast manager rather than a single toast component.

  • The push() helper is the shared internal mechanism for adding notifications. It generates a unique ID, appends a new toast object to the current array, and starts a timer that removes that same toast after 2500 milliseconds.

  • Keeping the timeout removal logic inside push() is an important design choice. It means the public helpers success(), error(), and info() remain focused on meaning and intent, while the provider centralizes the behavior of how toasts are created and removed.

  • The ID generation uses Date.now() + Math.random(), which is a lightweight way to make collisions extremely unlikely in this small notification system. For a temporary UI list like this, that is a practical and readable solution.

  • useCallback() memoizes push() so React does not recreate it unnecessarily on every provider render. This is useful because the context value depends on push(), and a stable callback helps avoid unnecessary context churn.

  • useMemo() is then used to create a stable context value object. Without it, consumers of the context would receive a freshly created object every render, even when the underlying behavior had not changed.

This section is where the toast provider quietly earns its value. The rest of the app gets a clean API, while timing and state management stay centralized in one place.

Rendering the Provider and Toast Stack

The final part of src/components/ui/Toast.tsx renders the provider tree and the actual visual toast stack. This is where the notification system becomes visible.

  return (
    <ToastContext.Provider value={value}>
      {children}
      <div className="fixed right-4 top-4 z-50 space-y-2">
        {toasts.map((toast) => (
          <div
            key={toast.id}
            className={
              toast.type === 'success'
                ? 'rounded-md bg-green-600 px-4 py-2 text-sm text-white shadow'
                : toast.type === 'error'
                ? 'rounded-md bg-red-600 px-4 py-2 text-sm text-white shadow'
                : 'rounded-md bg-gray-900 px-4 py-2 text-sm text-white shadow'
            }
          >
            {toast.message}
          </div>
        ))}
      </div>
    </ToastContext.Provider>
  );
}
  • The provider renders children and the toast stack together, which is exactly what we want. The toast system should wrap the app so any descendant can trigger notifications, but the notifications themselves should still be rendered by the provider.

  • The container uses fixed right-4 top-4 z-50, which places the stack in the top-right corner above the rest of the page. This is important because toasts should feel like temporary overlays, not permanent layout content that pushes the document around.

  • toasts.map(...) renders each active toast as a visual box. Since the state is an array, the provider can support multiple notifications at once in a simple, natural way.

  • The conditional className logic applies different colors depending on toast.type. That visual distinction matters because users can quickly recognize whether feedback is positive, negative, or informational without carefully rereading each message.

  • Even though the markup is small, this file provides a very useful global behavior. Its strength is not complexity; its strength is making feedback consistent and easy to trigger from anywhere else in the app.

Creating a Safe Toast Hook

At the bottom of the same file, useToast() provides a convenient way for components and hooks to consume the toast context.

export function useToast() {
  const context = useContext(ToastContext);
  if (!context) throw new Error('useToast must be used within ToasterProvider');
  return context;
}
  • useContext(ToastContext) is the actual React context lookup, but wrapping that inside a custom hook gives the rest of the codebase a much cleaner interface.

  • The explicit runtime error is a safety check that catches incorrect usage early. If a component tries to call useToast() outside of ToasterProvider, the app fails with a clear message instead of behaving unpredictably.

  • This pattern mirrors the custom hooks you have already built in earlier lessons: a shared provider defines state and behavior, and a small custom hook makes it ergonomic and safe to consume.

Defining a Shared Cart Storage Key

The next piece is small but important. The file src/lib/constants/cart.ts defines the one shared storage key the app uses when persisting the current cart ID in the browser.

export const CART_STORAGE_KEY = 'course-storefront-cart-id';
  • This file exists for consistency, not complexity. Any code that stores, restores, or clears the current cart should use the same key instead of hardcoding repeated string literals in multiple places.

  • A descriptive stable key makes local storage behavior easier to understand and easier to debug. If the name changed in different files, cart restoration would become fragile very quickly.

  • Small constants like this often look trivial, but they help keep the rest of the app from drifting into duplication and accidental mismatch.

Building the Cart API Service Layer

Now that toast infrastructure exists, the app also needs a cart-specific API helper module. The file src/lib/api/carts.ts translates user intent such as “add item,” “remove item,” or “checkout” into typed client calls.

This is an important layer in the frontend architecture. Components and hooks should not care about raw URLs or HTTP verbs. They should call expressive helpers that read like the language of the feature.

Importing Shared Client and Types

The top of src/lib/api/carts.ts imports the generic API client helpers and the types the cart API layer needs.

import { apiRequest, toRequestInit } from '@/lib/api/client';
import { AddCartItemInput, UpdateCartItemInput } from '@/types/api';
import { Cart, Order } from '@/types/domain';
  • apiRequest is the generic transport layer you built earlier in the course. Routing all cart requests through it keeps JSON parsing, envelope handling, and error behavior consistent with the rest of the storefront client code.

  • toRequestInit is the shared helper for building RequestInit objects. Using it here keeps request construction predictable and removes repetitive JSON body setup from each individual helper.

  • The DTO imports such as AddCartItemInput and UpdateCartItemInput ensure the request payloads stay typed. That means hooks and components can call cart helpers with confidence instead of guessing at the required body shape.

  • The domain imports Cart and Order define the expected response types. This is what helps the API helper layer stay focused on typed client calls rather than passing around unknown JSON.

Implementing the Cart Helpers

Managing Shared Cart State with a Provider Hook

The most substantial part of this lesson lives in src/lib/hooks/useCart.tsx. Despite the filename, this file is not just a small hook. It defines the entire cart context, provider, storage restoration logic, cart orchestration methods, and public consumer hook.

Its role is to make the rest of the storefront simpler. Pages and shared UI should not need to manage storage, stale-cart recovery, or cart request orchestration themselves.

We will walk through the file in three parts.

Defining Context Shape and Storage Helpers

The top of src/lib/hooks/useCart.tsx defines imports, the context contract, and the small local storage helpers used by the provider.

'use client';

import {
  createContext,
  ReactNode,
  startTransition,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from 'react';
import {
  addCartItem,
  checkoutCart,
  createCart as createCartRequest,
  getCart as getCartRequest,
  removeCartItem,
  setCartTaxCountry as setCartTaxCountryRequest,
  updateCartItem as updateCartItemRequest,
} from '@/lib/api/carts';
import { ApiClientError, getErrorMessage } from '@/lib/api/client';
import { CART_STORAGE_KEY } from '@/lib/constants/cart';
import { useToast } from '@/components/ui/Toast';
import { Cart, Order } from '@/types/domain';
interface CartContextValue {
  cart: Cart | null;
  cartId: string | null;
  itemCount: number;
  isReady: boolean;
  isLoading: boolean;
  errorMessage: string | null;
  ensureCart: () => Promise<string>;
  refreshCart: (nextCartId?: string) => Promise<Cart | null>;
  addItem: (productId: string, quantity: number) => Promise<void>;
  updateItem: (itemId: string, quantity: number) => Promise<void>;
  removeItem: (itemId: string) => Promise<void>;
  setTaxCountry: (countryCode: string) => Promise<void>;
  checkout: () => Promise<Order>;
  clearCart: () => void;
}

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

function readStoredCartId() {
  if (typeof window === 'undefined') return null;
  return window.localStorage.getItem(CART_STORAGE_KEY);
}

function persistCartId(cartId: string | null) {
  if (typeof window === 'undefined') return;
  if (!cartId) {
    window.localStorage.removeItem(CART_STORAGE_KEY);
    return;
  }
  window.localStorage.setItem(CART_STORAGE_KEY, cartId);
}
  • The 'use client' directive is required because this provider depends on React state, effects, context, browser storage, and other client-only behavior. Cart restoration is a browser concern, not a server-render concern.

  • CartContextValue is the full public interface the rest of the app receives. Notice that it includes not just raw state like cart and cartId, but also derived convenience like itemCount, readiness flags, and meaningful actions like addItem() and checkout().

  • Exposing isReady is especially important. The cart may need a short restoration phase on mount, and shared UI should know when that initial check has completed so it does not render misleading values during hydration.

  • readStoredCartId() and persistCartId() keep local storage behavior centralized. Their job is intentionally simple: read the saved cart ID, write a cart ID, or remove it entirely when the cart should no longer be persisted.

  • The typeof window === 'undefined' checks protect the code from trying to access browser APIs in environments where window does not exist. This is a normal and important pattern in client-aware Next.js code.

These helpers may look small, but they are essential because cart persistence is the bridge between a short-lived React tree and a shopper’s longer-lived browsing session.

Provider State, Recovery, and Cart Restoration

The middle section of the file initializes provider state and implements the logic that restores, refreshes, and clears the cart.

export function CartProvider({ children }: { children: ReactNode }) {
  const [cart, setCart] = useState<Cart | null>(null);
  const [cartId, setCartId] = useState<string | null>(null);
  const [isReady, setIsReady] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const initializedRef = useRef(false);
  const toast = useToast();

  const clearCartState = useCallback(() => {
    persistCartId(null);
    startTransition(() => {
      setCart(null);
      setCartId(null);
      setErrorMessage(null);
    });
  }, []);

  const handleRecoverableCartError = useCallback(
    (error: unknown) => {
      if (error instanceof ApiClientError && (error.status === 404 || error.status === 409)) {
        clearCartState();
        return true;
      }
      return false;
    },
    [clearCartState]
  );

  const refreshCart = useCallback(
    async (nextCartId?: string) => {
      const resolvedCartId = nextCartId ?? cartId ?? readStoredCartId();
      if (!resolvedCartId) {
        startTransition(() => {
          setCart(null);
          setCartId(null);
          setErrorMessage(null);
        });
        return null;
      }

      setIsLoading(true);
      try {
        const nextCart = await getCartRequest(resolvedCartId);
        if (nextCart.status !== 'open') {
          clearCartState();
          return null;
        }
        persistCartId(nextCart.id);
        startTransition(() => {
          setCart(nextCart);
          setCartId(nextCart.id);
          setErrorMessage(null);
        });
        return nextCart;
      } catch (error) {
        if (!handleRecoverableCartError(error)) {
          startTransition(() => {
            setErrorMessage(getErrorMessage(error));
          });
        }
        return null;
      } finally {
        setIsLoading(false);
      }
    },
    [cartId, clearCartState, handleRecoverableCartError]
  );
  const ensureCart = useCallback(async () => {
    if (cartId) return cartId;

    setIsLoading(true);
    try {
      const nextCart = await createCartRequest();
      persistCartId(nextCart.id);
      startTransition(() => {
        setCart(nextCart);
        setCartId(nextCart.id);
        setErrorMessage(null);
      });
      return nextCart.id;
    } finally {
      setIsLoading(false);
    }
  }, [cartId]);

  useEffect(() => {
    if (initializedRef.current) return;
    initializedRef.current = true;

    const existingCartId = readStoredCartId();
    if (!existingCartId) {
      setIsReady(true);
      return;
    }

    void refreshCart(existingCartId).finally(() => setIsReady(true));
  }, [refreshCart]);
  • The provider state tells the rest of the app everything it may need to know about the cart at any moment: the current cart snapshot, the current ID, whether initial restoration has completed, whether a request is in flight, and whether a visible error exists.

  • clearCartState() is very important because it removes the cart from both React state and local storage. This ensures stale, missing, or completed carts disappear cleanly instead of lingering in the UI or browser storage after they are no longer valid.

  • startTransition() is used when updating non-urgent state derived from cart operations. That helps communicate that these updates do not need to block more urgent rendering work, which is a nice fit for provider-level orchestration like this.

  • handleRecoverableCartError() treats 404 and 409 cart errors as recoverable cases. That is a very user-friendly choice: if the cart no longer exists or is no longer usable, the app quietly resets cart state instead of always surfacing a loud error state.

  • refreshCart() is the main cart snapshot synchronizer. It resolves the best cart ID source, trying nextCartId, then existing provider state, then local storage. This means callers do not need to manually reason about where the cart ID should come from.

  • If there is no cart ID at all, refreshCart() clears cart-related state and returns null. That makes the empty-cart case explicit and keeps the rest of the provider predictable.

  • When a cart is successfully fetched, the provider also verifies that its status is still 'open'. If the backend returns a cart that has already been checked out or otherwise closed, the provider clears it from state and storage instead of continuing to treat it as the active shopper cart.

  • The catch branch distinguishes recoverable stale-cart scenarios from other errors. Recoverable cases are quietly reset; non-recoverable failures update errorMessage with a user-facing string from getErrorMessage().

  • ensureCart() is the helper that makes add-to-cart flows simpler. Instead of forcing every consumer to check whether a cart already exists, it creates one only when necessary and returns the resolved cart ID.

  • The mount-time useEffect() restores the cart once and then sets isReady when that first restoration process is complete. This flag matters a lot for shared UI like the navbar, because the app should not flash an incorrect cart count before restoration finishes.

This middle section is the heart of the cart provider. It is doing the storage recovery, validity checks, and orchestration work so that other parts of the app do not have to.

Cart Actions, Checkout, and Provider Value

The final section defines the action methods that the rest of the app will use and then returns the context provider.

  const addItem = useCallback(
    async (productId: string, quantity: number) => {
      const resolvedCartId = await ensureCart();
      await addCartItem(resolvedCartId, { product_id: productId, quantity });
      await refreshCart(resolvedCartId);
    },
    [ensureCart, refreshCart]
  );

  const updateItem = useCallback(
    async (itemId: string, quantity: number) => {
      if (!cartId) return;
      await updateCartItemRequest(cartId, itemId, { quantity });
      await refreshCart(cartId);
    },
    [cartId, refreshCart]
  );

  const removeItem = useCallback(
    async (itemId: string) => {
      if (!cartId) return;
      await removeCartItem(cartId, itemId);
      await refreshCart(cartId);
    },
    [cartId, refreshCart]
  );

  const setTaxCountry = useCallback(
    async (countryCode: string) => {
      if (!cartId) return;
      const nextCart = await setCartTaxCountryRequest(cartId, countryCode);
      startTransition(() => {
        setCart(nextCart);
        setErrorMessage(null);
      });
    },
    [cartId]
  );

  const checkout = useCallback(async () => {
    const resolvedCartId = cartId ?? readStoredCartId();
    if (!resolvedCartId) {
      throw new Error('Your cart is empty.');
    }

    const order = await checkoutCart(resolvedCartId);
    clearCartState();
    toast.success('Order placed successfully.');
    return order;
  }, [cartId, clearCartState, toast]);

  return (
    <CartContext.Provider
      value={{
        cart,
        cartId,
        itemCount: cart?.items?.reduce((total, item) => total + item.quantity, 0) ?? 0,
        isReady,
        isLoading,
        errorMessage,
        ensureCart,
        refreshCart,
        addItem,
        updateItem,
        removeItem,
        setTaxCountry,
        checkout,
        clearCart: clearCartState,
      }}
    >
      {children}
    </CartContext.Provider>
  );
}
export function useCart() {
  const context = useContext(CartContext);
  if (!context) throw new Error('useCart must be used within CartProvider');
  return context;
}
  • addItem() is a great example of the provider simplifying the rest of the app. A caller does not need to know whether a cart already exists. It just asks to add a product and quantity, while ensureCart() and refreshCart() handle the necessary orchestration.

  • updateItem() and removeItem() are similarly expressive. They translate user-level cart actions into the necessary API calls, then immediately refresh the cart snapshot so provider state stays current.

  • setTaxCountry() updates cart tax country through the API layer and then stores the returned cart snapshot directly in state. This avoids forcing pages to manually refetch or reconstruct tax-related updates.

  • checkout() resolves the current cart ID, throws a meaningful error if no cart exists, performs checkout, clears cart state, and triggers a success toast. This is a nice example of multiple shared systems working together: cart service logic and toast feedback cooperate cleanly because both live in properly ordered providers.

  • The context value includes a derived itemCount computed from cart items. This is exactly the kind of prepared value shared UI wants: consumers should not have to recalculate item totals themselves every time they need to display a badge.

  • The final useCart() hook mirrors the useToast() pattern. It wraps useContext() in a small ergonomic API and includes a safety check that makes misuse outside CartProvider fail clearly.

This file succeeds when other components become simpler, and that is exactly what it achieves.

Composing Providers in the Correct Order

Now that the toast system and cart provider both exist, src/components/providers/AppProviders.tsx needs to compose them correctly.

'use client';

import { ReactNode } from 'react';
import { ToasterProvider } from '@/components/ui/Toast';
import { CartProvider } from '@/lib/hooks/useCart';

export function AppProviders({ children }: { children: ReactNode }) {
  return (
    <ToasterProvider>
      <CartProvider>{children}</CartProvider>
    </ToasterProvider>
  );
}
  • The most important detail here is the order: ToasterProvider wraps CartProvider. That matters because cart logic eventually calls useToast(), and a hook cannot consume a provider that has not been mounted above it in the tree.

  • This file continues the same role it had in earlier lessons: composing shared client-side providers while keeping layout concerns elsewhere. It is infrastructure, not page structure.

  • The provider tree now gives the whole app two important global capabilities: notification feedback and persistent cart state.

Making the Navbar Cart-Aware

With the cart provider in place, the shared shell can become state-aware without being rewritten. The file src/components/layout/Navbar.tsx now reads prepared cart values from useCart() and shows them in the cart badges.

Importing the Cart Hook and Reading Shared State

At the top of the file, the navbar now imports and consumes the cart provider.

'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
import { PageContainer } from '@/components/layout/PageContainer';
import { useCart } from '@/lib/hooks/useCart';

export function Navbar() {
  const pathname = usePathname();
  const { itemCount, isReady } = useCart();
  • useCart() gives the navbar exactly the values it needs: itemCount and isReady. This is a sign the provider is doing its job well. The navbar does not need to know how cart restoration works, how local storage is read, or how item totals are calculated.

  • isReady is especially useful during the cart restoration phase. Before the initial storage check completes, the navbar should avoid showing a possibly incorrect item count.

This is a strong example of shared UI becoming smarter through better data sources rather than through more complex structure.

Updating the Mobile and Desktop Cart Links

The rest of the navbar mostly preserves its existing layout, but the cart badge placeholders are now replaced with live shared values.

  return (
    <header className="sticky top-0 z-40 border-b border-stone-200/80 bg-[#f8f4ec]/90 backdrop-blur">
      <PageContainer className="flex flex-col gap-4 py-4 md:flex-row md:items-center md:justify-between">
        <div className="flex items-center justify-between gap-4">
          <Link href="/" className="flex items-center gap-3">
            <div>
              <p className="font-serif text-xl tracking-tight text-stone-950">Codesignal E-commerce Simulator</p>
              <p className="text-xs uppercase tracking-[0.3em] text-stone-500">Modern Goods</p>
            </div>
          </Link>
          <Link
            href="/cart"
            className="inline-flex items-center gap-2 rounded-full border border-stone-200 bg-white px-4 py-2 text-sm font-medium text-stone-900 md:hidden"
          >
            Cart
            <span className="rounded-full bg-stone-900 px-2 py-0.5 text-xs text-white">{isReady ? itemCount : '...'}</span>
          </Link>
        </div>
        <nav className="flex flex-wrap items-center gap-2">
          {navItems.map((item) => {
            const active = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
            return (
              <Link
                key={item.href}
                href={item.href}
                className={clsx(
                  'rounded-full px-4 py-2 text-sm transition-colors',
                  active ? 'bg-stone-900 text-white' : 'text-stone-600 hover:bg-white hover:text-stone-900'
                )}
              >
                {item.label}
              </Link>
            );
          })}
        </nav>
        <Link
          href="/cart"
          className="hidden items-center gap-2 rounded-full border border-stone-200 bg-white px-4 py-2 text-sm font-medium text-stone-900 md:inline-flex"
        >
          Cart
          <span className="rounded-full bg-stone-900 px-2 py-0.5 text-xs text-white">{isReady ? itemCount : '...'}</span>
        </Link>
      </PageContainer>
    </header>
  );
}
  • The overall navbar structure remains almost unchanged, which is exactly the right outcome here. This lesson is about enriching the shell with shared state, not redesigning the header from scratch.

  • Both the mobile and desktop cart links now display the live itemCount. Because the count comes from the cart provider, both badge locations stay in sync automatically.

  • The conditional {isReady ? itemCount : '...'} is a thoughtful detail. It prevents the navbar from flashing a wrong number during initial restoration and then snapping to the real value a moment later.

  • This kind of improvement is typical of well-designed shared UI. The markup barely changes, but the component becomes more useful because its data source improves.

Recap

In this lesson, you built two important pieces of shared storefront infrastructure: toast notifications and persistent cart state.

You started with src/components/ui/Toast.tsx, where the provider manages the active toast list, the shared push() helper, timed removal, stable context helpers through useMemo(), and the fixed toast stack overlay. Then you added src/lib/constants/cart.ts so the cart storage key stays stable and centralized.

Next, you created the cart service layer in src/lib/api/carts.ts, where helpers like createCart(), addCartItem(), removeCartItem(), and checkoutCart() translate feature intent into typed client calls. After that, src/lib/hooks/useCart.tsx brought the cart feature together by managing provider state, local storage restoration, stale-cart recovery, cart refreshing, cart creation on demand, checkout feedback, and a clean public hook interface.

Finally, you composed the providers correctly in src/components/providers/AppProviders.tsx by mounting ToasterProvider above CartProvider, and you updated src/components/layout/Navbar.tsx so the shared shell can display a live cart badge using itemCount and isReady.

The main takeaway is that shared infrastructure should remove work from the rest of the app. Toasts centralize global feedback. Cart helpers centralize API behavior. The cart provider centralizes restoration and orchestration. And the navbar simply consumes prepared values. That is the same architectural pattern you have been strengthening throughout the course: let lower-level shared systems handle the mechanics so the UI can stay focused and readable.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal