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()anderror() - 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.
-
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
Toasttype describes one notification object. Each toast needs anidso React can track it reliably in a list, amessagefor the visible text, and atypeso the UI can style success, error, and informational notifications differently. -
ToastContextValueintentionally 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 likesuccess(message),error(message), andinfo(message). -
createContext<ToastContextValue | null>(null)creates the shared context that will later be filled by the provider. Starting withnullis 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.
-
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 after2500milliseconds. -
Keeping the timeout removal logic inside
push()is an important design choice. It means the public helperssuccess(),error(), andinfo()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()memoizespush()so React does not recreate it unnecessarily on every provider render. This is useful because the context value depends onpush(), 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.
-
The provider renders
childrenand 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.
-
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 ofToasterProvider, 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.
-
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.
-
apiRequestis 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. -
toRequestInitis the shared helper for buildingRequestInitobjects. Using it here keeps request construction predictable and removes repetitive JSON body setup from each individual helper. -
The DTO imports such as
AddCartItemInputandUpdateCartItemInputensure 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
CartandOrderdefine 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.
-
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. -
CartContextValueis the full public interface the rest of the app receives. Notice that it includes not just raw state likecartandcartId, but also derived convenience likeitemCount, readiness flags, and meaningful actions likeaddItem()andcheckout(). -
Exposing
isReadyis 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()andpersistCartId()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 wherewindowdoes 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.
-
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()treats404and409cart 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, tryingnextCartId, 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 returnsnull. 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
catchbranch distinguishes recoverable stale-cart scenarios from other errors. Recoverable cases are quietly reset; non-recoverable failures updateerrorMessagewith a user-facing string fromgetErrorMessage(). -
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 setsisReadywhen 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.
-
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, whileensureCart()andrefreshCart()handle the necessary orchestration. -
updateItem()andremoveItem()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
itemCountcomputed 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 theuseToast()pattern. It wrapsuseContext()in a small ergonomic API and includes a safety check that makes misuse outsideCartProviderfail 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.
-
The most important detail here is the order:
ToasterProviderwrapsCartProvider. That matters because cart logic eventually callsuseToast(), 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.
-
useCart()gives the navbar exactly the values it needs:itemCountandisReady. 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. -
isReadyis 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.
-
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.
