ProNextJS
    solution

    Replacing Cart Context with Redux

    Jack HerringtonJack Herrington

    Start by copying 03-cart-context-with-initial-state into 05-cart-with-redux:

    cp -r 03-cart-context-with-initial-state 05-cart-with-redux
    

    After setting up the new project, let's return to our code editor.

    Removing Cart Context & Setting Up Redux

    The first thing we'll do is remove the CartContext component at app/components/CartContext.tsx to make sure we replace it completely with Redux.

    Next, we need to add the necessary libraries for Redux:

    • redux: The core state management library.
    • react-redux: Provides bindings between React and Redux, selectors, and the store provider.
    • @reduxjs/toolkit: A library that simplifies building and maintaining Redux stores.
    npm install redux react-redux @reduxjs/toolkit
    

    With the dependencies installed, we'll create a store directory inside of the app directory, and a new store.ts file that will be our Redux store.

    Creating the Cart Slice & Store

    Inside of store.tsx, we'll have two slices for the cart and the reviews.

    Let's start by creating the cart slice.

    First, we'll import the createSlice from Redux Toolkit, as well as our Cart type from @/api/types:

    import { createSlice } from "@reduxjs/toolkit";
    import { Cart } from "@/api/types";
    

    We'll define an interface for the CartState, and use createSlice to create a slice named cart with the initial state of an empty products array:

    export interface CartState {
    	cart: Cart;
    }
    
    const initialState: CartState = {
    	cart: {
    		products: [],
    	},
    };
    
    export const cartSlice = createSlice({
    	name: "cart", // Red squiggles!
    	initialState, // Red squiggles!
    });
    

    At this point, there are red squiggles inside of the cartSlice because we haven't added any reducers.

    Reducers allow us to specify the actions for a given slice. Let's bring in some reducers.

    In this case, we'll add the setCart action, which takes a cart and sets the cart in the store to that cart. We'll also need to import PayloadAction type in order to properly define the reducer:

    import type { PayloadAction } from "@reduxjs/toolkit";
    ...
    
    export const cartSlice = createSlice({
      name: "cart",
      initialState,
      reducers: {
        setCart: (state, action: PayloadAction<Cart>) => {
          state.cart = action.payload;
        },
      },
    });
    

    Now that we have our slice set up, we need to create a store.

    To do this, let's import configureStore from the Redux toolkit:

    import { configureStore } from "@reduxjs/toolkit";
    

    According to the ReduxJS toolkit documentation, you would typically build your store like this:

    const store = configureStore({
    	reducer: {
    		cart: cartSlice.reducer,
    	},
    });
    

    However, we want to avoid this because it creates a global variable!

    Instead, we'll create a createStore function that in turn will call configureStore:

    export const createStore = () =>
    	configureStore({
    		reducer: {
    			cart: cartSlice.reducer,
    		},
    	});
    

    Next, we'll export some types for the StoreType, the RootState, and the AppDispatch.

    export type StoreType = ReturnType<typeof createStore>;
    export type RootState = ReturnType<StoreType["getState"]>;
    export type AppDispatch = StoreType["dispatch"];
    

    The RootState is the most important type here, as it represents the structure of our store. Hovering over it shows us that it contains the cart with the CartState:

    // hovering over RootState
    
    type RootState = {
    	cart: CartState;
    };
    

    Finally, we will create a selector for useCart that will give us access to the cart.

    The selector will use the useSelector hook from react-redux to get the cart from the store:

    import { useSelector } from "react-redux";
    ...
    export const useCart = () => useSelector((state: RootState) => state.cart.cart);
    

    At this point, we have created our Redux store, and are ready to start using it!

    Creating a Store Provider

    Since we can't use Redux globally, we will use context with the built-in provider from react-redux to pass our store throughout the entire application.

    First, we'll create a new StoreProvider.tsx file inside of the store directory.

    Since we will be using context, the StoreProvider needs to be a client component. The component will need to create and hold the store, which we'll do with useRef. If the store ref doesn't exist, we'll create it with createStore and dispatch the setCart action with the initial state. Finally, the StoreProvider will return the children wrapped by the Provider from react-redux with the current store passed in as a prop:

    "use client";
    import { useRef } from "react";
    import { Provider } from "react-redux";
    import { type Cart } from "@/api/types";
    import { createStore, setCart } from "./store";
    
    export default function StoreProvider({
    	cart,
    	children,
    }: {
    	cart: Cart;
    	children: React.ReactNode;
    }) {
    	const storeRef = useRef<ReturnType<typeof createStore> | null>(null);
    	if (!storeRef.current) {
    		storeRef.current = createStore();
    		storeRef.current.dispatch(setCart(cart));
    	}
    
    	return <Provider store={storeRef.current}>{children}</Provider>;
    }
    

    Now that we have a working StoreProvider, we can update our components to use it.

    Updating the Layout Component

    In the Layout component at layout.tsx, import the StoreProvider and replace the existing CartProvider with it.

    Where we used to use cart from the CartContext, we will now use cart from the store:

    import StoreProvider from "./store/StoreProvider";
    
    // inside the RootLayout return:
    ...
    <body className={inter.className}>
      <StoreProvider cart={cart}>
        <Header clearCartAction={clearCartAction} />
        <main className="mx-auto max-w-3xl">{children}</main>
      </StoreProvider>
    </body>
    

    Updating the Cart Popup Component

    Inside of CartPopup.tsx, we will replace the useCart import from CartContext with the useCart from the store.

    We'll also need to import dispatch from react-redux in order to dispatch the setCart action which also comes from the store when the cart is cleared:

    "use client";
    import { useDispatch } from "react-redux";
    import { type Cart } from "@/api/types";
    import { useCart, setCart } from "../store/store";
    

    We'll get the cart from useCart(), and dispatch from useDispatch():

    const cart = useCart();
    const dispatch = useDispatch();
    

    Then, when the "Clear Cart" button is clicked, we'll dispatch the setCart action with the output of the clearAction function:

    // inside the CartPopop return:
    
    <button
    	className="..."
    	onClick={async () => {
    		dispatch(setCart(await clearCartAction()));
    	}}
    >
    	Clear Cart
    </button>
    

    Updating the Add to Cart Component

    Over in AddToCart.tsx, we only need to bring in the setCart action from the store. We'll also import useDispatch from react-redux to dispatch the action when the button is clicked:

    "use client";
    import { useDispatch } from "react-redux";
    import { type Cart } from "@/api/types";
    import { setCart } from "@/app/store/store";
    
    // inside the AddToCart return:
    ...
      const dispatch = useDispatch();
      return (
        <button
          className="..."
          onClick={async () => {
            dispatch(setCart(await addToCartAction()));
          }}
        >
          Add To Cart
        </button>
      );
    }
    

    Recapping Our Work

    Back in the browser we can test our updated components. The cart count updates correctly, and the cart pop-up displays the correct items.

    Taking a look at the SSR result by viewing the page source, you'll notice a <span> tag with the number 2 nested inside, representing the current value of the cart.

    The cart can also be cleared, and the count updates accordingly!

    This confirms that the Redux store is being initialized before the server-side render, and the correct data is being passed to the client components.

    Now that we've successfully created our Redux store and passed it around using a StoreProvider at the layout level, we're ready to tackle the next challenge of integrating the Reviews slice into our store.

    Transcript

    First thing we're going to do is recursively copy 03-cart-context-with-initial-state into 05-cart-with-redux. Now we'll get that all set up and running. And that looks pretty good. Okay, so let's go back into our code.

    Now the first thing I'm going to do is remove the cart-context component because we want to make sure that we have replaced that completely and so nothing will compile without that. Now the next thing we're going to do is create a store, a Redux store. Next thing we're going to do is we're going to add the libraries for Redux.

    That would be Redux, which is a core of the state management system, React Redux, which provides the bindings between React and Redux, the selectors to go and get the data, for example, as well as the store provider, and then the ReduxJS toolkit, which is the library that makes it really easy to build and maintain Redux stores.

    Now that we have all that installed, let's go create a store directory, as well as a store in our app directory. So what are we going to have in our store? We're going to have two things. We're going to have the cart, and we're also going to have the review. So let's start off with the cart. So we need to bring in the type of cart, and then we're going to define some initial state.

    So this would be the structure of the slice for the cart. We're going to actually have two different slices in our single Redux store, one for the cart and one for the reviews. It's just a nice way to kind of keep the two from intermingling.

    So we're going to define our cart slice as having just a cart in it, and we'll define an initial state that has an empty cart. So now we need to create our cart slice, so we're going to bring in create slice from the Redux toolkit, and we're going to use create slice to create a slice named cart with that initial state.

    Now it's giving us the red squigglies because we haven't put any reducers into our slice. Reducers allow us to specify the actions for that given slice, so we'll bring in some reducers. So we'll bring in the set cart action. That action is just going to take a cart and set the cart in the store to that cart. Pretty easy.

    We need to bring in payload action so we can properly define that reducer. So now that we have our slice all set up, now we just need to be able to create a store. So we need to bring in configure store also from the Redux toolkit. So if you read the ReduxJS toolkit documentation, it would tell you to build your store this way.

    You define a global variable called store, and you use configure store to create that variable. Now we're actually doing the right thing when it comes to calling configure store. We're giving it the right reducer, the cart slice, all that. That's fine. The issue is that we're creating a global variable. We don't want to do that.

    So we want to instead create a create store function that in turn calls configure store. All right, now we have our create store function, which is awesome. Now we just need to go and expose a bit more so that we can access that function.

    Another thing we want to export is the set cart action so we can use that externally to set the cart. We also want to export some types. The most important type here is root state. That is the structure of our store. So we do command K and command I.

    On that, we can see that the current store has just cart on it, and within that cart is the cart state. And then finally, the best place to put a selector is kind of in this store file here. So we're going to create a selector for use cart. That's just going to give us access to the cart.

    Of course, in order to make that work, we need to bring in use selector. And there we go. Now we've got a good use cart. So in terms of the Redux store itself, we're good to go.

    Now we get to the really fun part, which is how do we create and distribute this Redux store when we can't just declare it globally like we could before? Well, we can use context for that.

    So just like we passed down context in our React state implementation, how about we use that context mechanism to instead define a store and then use

    that built-in store provider provided by React Redux to actually pass it down throughout the entire system? Well, let's give it a go. So create a new file called store provider, and we'll start off with a store provider client component.

    Now, this needs to be a client component because we're going to use that provider to provide the store down, and that provider uses context. So this needs to be a client component. So the next thing we need to do is create and hold the store. Now, there's two different ways we can do that. You can use use state or you can use use ref. I'm just going to choose to use use ref.

    And then down here, I'll create a store ref that's going to hold the store. Of course, we need to bring in create store in order to create the store. So far, so good. So let's create the store. I always say if we don't have a store, well, let's create a store and then set that to current.

    Now, you probably don't need to make that check because where we're going to put it in the layout is probably never going to get re-rendered. But it's okay to make that check anyway. Now, let's send the store to the provider so they can provide it down to any components. But one last thing we need to do is initialize the store.

    So let's go and dispatch. All right, so far, so good. So let's go into our layout and bring in our store provider. And then we change out our cart provider for our store provider. So far, so good.

    Okay, now we can go into our client components like header, cart pop-up, and add to cart, and then use our store provider. So where we used to use cart from the cart context, now we use cart from the store. And we're not going to go back in array, we're just going to get back the cart. That was pretty easy.

    Okay, let's fix cart pop-up. So again, we're not going to get used cart from cart context, we're going to get it from the store. We'll just get the cart. But how do we dispatch that set cart action? So when you clear the store, we're going to be resetting the cart. So how do we go and update the cart?

    Well, we bring in use dispatch from React Redux. We then use it to get the dispatch function. We also need to get set cart.

    And so we'll dispatch the set cart, which will go and format the action payload that we're going to send to our reducer with the output of clear action. Okay. And then finally, let's go over and check out add to cart. So all we need here is set cart apparently.

    So we need that dispatch. And we'll get that dispatch, and then we'll dispatch the set cart. Okay, not bad. Let's check it out and see if it works. All right. Seems to be kind of okay. Let's add to cart. Now it went from zero to three. Aha.

    So everything looks good. Let's bring up the cart and make sure everything looks fine. We can clear the cart, refresh, zero, that's true. So the only issue is, like we had with our initial React state version, we're not initializing the data properly.

    So let's go back and take a look at our store provider to see if we can initialize that state. So maybe what we need to do, because we have the cart already, is just dispatch the set cart action to the store that we just created. Let's give that a try.

    So you need to bring in set cart. Now let's rerun the server. So I'm starting with a couple of cart items. And let's try it out. If I hit refresh, then we start off at two. That's awesome. And actually, let's go take a look at our SSR result.

    And we can see in the SSR result, in this haze of all these HTML tags, we have one span tag that has two in it, and that is the current value of the cart. So our Redux store is being initialized before the server-side render, and we're getting the right data in the client components, and that's all going out in the initial server-side render.

    So, so far, so good. Okay, now we've created our Redux store, and we're passing it around using a store provider at the layout level. Now, when we add in our reviews slice into our store, it's going to get kind of interesting as we work through some of these issues.

    So don't stop now. Jump into the next exercise, and let's implement the reviews portion of our app.