ProNextJS
    Loading
    solution

    Creating a Cart Context and Provider

    Jack HerringtonJack Herrington

    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.

    Transcript

    So I've copied the contents of the starting point directory into this new cart context directory, and that's where I'm going to start my implementation. I'm going to start by creating our cart context. I'll do that over in the components directory. Create a new file called cart context. Now, this is going to be a client component because it's going to have context,

    and you can't have context in an RSC. Next, I'm going to bring in create context as well as useState. We're going to use useState to manage the cart. I'm also going to bring in a type of the cart. Now, to make it easy on myself, I'm going to create a custom hook called useCartState. It's just going to invoke useState and create a simple cart to start out with.

    So we need a cart context in order to provide it. In this case, I will use the cart context that has the return type of the useCartState custom hook function that I just created. Now, the next thing that I like to do is create the hook that I'm going to use to access the cart. I'll call that useCart,

    and all we're going to do is just use the context hook, useContext, to get the context from the cart context. Maybe don't find that cart context and we'll just throw an error. Otherwise, we'll return the output of the cart, which is going to be an array that has the cart as well as the cart setter, just a standard output of a useState. And then finally, I'm going to define the cart provider.

    Now, that's the client component that is going to wrap any components downstream, the children, in the provider that's going to provide to them that cart. Now, the really cool thing about client components in the App Writer is that they can transclude

    or project children that are either client components or RSCs, which is really great. Okay, so now that we have our cart provider, we're going to want to go to the layout and actually use our cart provider. So we'll start by bringing in our cart provider, and then within the body,

    we'll wrap the contents within that cart provider. But now, since we already have that cart, do we really need to send it to the header? Well, no, we don't. So we can get rid of it from the header, and then go over to our header, and it'll bring in useCart from the cart context. We'll get rid of it as a prop, and then we'll just get it from that useCart hook.

    Easy peasy. Same kind of thing with our cart pop-up. But in this case, we both need to bring in that cart because we want to display it, but we also want to set the cart based on the output of the clearCart action. 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. Now, we still have an issue over in header.

    Let's go remove the cart here. And I think the last thing we need to do is over in addToCart. We need to bring in useCart. And then all we're gonna use here is that setCart because we're gonna setCart to the output of the addToCart action. All right, let's have a look. So we start off with zero. Now, that's because our cart has zero products in it.

    So that's actually a good sign. Now, let's go into our product, add it to the cart. Now, we can see that it jumps to three, and that's really interesting. So let's go have a look. Well, it did accurately add the ELF t-shirt to our cart. That's great. So why did it get to three? Well, if you look in the server, in our API, our cart API, we have an initial cart setup

    that has two products in it already. So that's why you have three. Once we did the addToCart, we added the ELF t-shirt to our in-memory cart. That gave us three. We got the cart back correctly. So that's actually looking really good. Now, as you'll notice, as I browse around, that cart remains consistent at three everywhere I go.

    And that's because that one cart context is shared between every single route in the application, including the homepage, as well as any product detail page. But if I refresh, it goes back to zero again. So our next step is to go make sure that we seed the data in that cart context properly with the initial data from the server.

    And we'll do that in our next exercise.