Replacing Cart Context with Redux
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.