Creating a Cart Context and Provider
The first step is to implement a Cart Context, which will be in a file CartContext.tsx
in the src/app/components/
directory.
Implementing a Cart Context
The CartContext
will be a Client Component so we'll start the file with "use client";
at the top. Remember, React Server Components don't support context.
We'll import React, createContext
, and useState
, as well as importing the Cart
type:
"use client";
import React, { createContext, useState } from "react";
import { type Cart } from "@/api/types";
To make things easier, we'll create a custom hook called useCartState
that will invoke useState
:
const useCartState = () =>
useState<Cart>({
products: [],
});
Then we'll create a CartContext
in order to provide it. The return type will be the useCartState
custom hook:
export const CartContext = createContext<ReturnType<
typeof useCartState
> | null>(null);
Now we'll create the useCart
hook that will be used to access the cart. It will use the useContext
hook to get the context from the CartContext
. Its output will be an array with the cart and its setter, or it will throw an error.
export const useCart = () => {
const cart = React.useContext(CartContext);
if (!cart) {
throw new Error("useCart must be used within a CartProvider");
}
return cart;
};
Finally, we'll define the CartProvider
. This will be a client component that will wrap any downstream children that it's going to provide them the cart:
const CartProvider = ({ children }: { children: React.ReactNode }) => {
const [cart, setCart] = useCartState();
return (
<CartContext.Provider value={[cart, setCart]}>
{children}
</CartContext.Provider>
);
};
What's cool about client components in the App Router is that they can transclude (or "project") children that are either client components or RSCs.
Updating the Layout Component
Now that we have our CartProvider
, we'll need to update the Layout
to use it.
Inside of Layout.tsx
, import the CartProvider
then wrap the Header
and main
content with it.
Note that we can remove the cart
prop from the Header
since it will be provided by the context!
// inside layout.tsx
import CartProvider from "./components/CartContext";
...
<CartProvider>
<Header clearCartAction={clearCartAction} />
<main className="mx-auto max-w-3xl">{children}</main>
</CartProvider>
...
Updating the Header Component
Inside of Header.tsx
, we can import the useCart
hook from CartContext
:
import { useCart } from "./CartContext";
Then we'll get rid of the cart
prop and get it from the useCart
hook instead:
export default function Header({
clearCartAction,
}: {
clearCartAction: () => Promise<Cart>;
}) {
const [cart] = useCart();
...
We'll also remove the cart
prop from the CartPopup
component:
...
{showCart && <CartPopup clearCartAction={clearCartAction}>}
...
Updating the Cart Popup Component
The same kind of thing needs to be done with our CartPopup
component at CartPopup.tsx
.
In this case, we both need to bring in the cart because we want to display it, but we also want to set the cart based on the output of the clearCartAction
event handler. So, we'll bring in setCart
as well. And then down here, we will set the cart based on the output of the clearCart
action.
import { useCart } from "./CartContext";
...
export default function CartPopup({
clearCartAction,
}: {
clearCartAction: () => Promise<Cart>;
}) {
const [cart, setCart] = useCart();
...
Update the Add To Cart Component
We also need to bring in useCart
to the Add to Cart component at AddToCart.tsx
.
This time we'll bring in just setCart
from the hook, since the cart will be set to the output of the addToCartAction
:
export default function AddToCart({
addToCartAction,
}: {
addToCartAction: () => Promise<Cart>;
}) {
const [, setCart] = useCart();
...
Checking Our Work
Checking the Donuts & Dragoons app, we can see a 0
in the header because we haven't added anything to the cart. However, when we navigate into a product and add it to the cart, the count jumps to 3
!
Let's jump back to our code to determine why.
Inside of src/api/cart.ts
we can see that the starting state of the cart already includes two items:
const cart: Cart = {
products: [
{
id: 1,
name: "Castle T-Shirt",
image: "/castle-t-shirt.jpg",
price: 25,
},
{
id: 2,
name: "Dragon T-Shirt",
image: "/dragon-t-shirt.jpg",
price: 25,
},
],
};
This means that when we performed the addToCart
action, it added the Elf t-shirt to our in-memory cart, so the total count became three.
Browsing around the site, the cart count remains consistent because the cart context is shared between every single route in the application, including the homepage and any product detail page.
However, when we refresh the page, the cart count goes back to zero.
In the next exercise, we'll work on seeding the cart context with the initial data from the server in order to maintain the correct cart count.