Implement Zustand for State Management
 Jack Herrington
Jack HerringtonInstall Zustand by running the following command:
npm install zustand
Because we can't create a global variable when using App Router, we need to use React Context to teleport the hook wherever it is needed.
Let's start by creating the cart provider.
Creating the Cart Provider
Inside of the app directory, create a new store directory then a file named CartProvider.tsx.
At the top of the file, start by specifying this will be a client component, then import the necessary functions for React context. Next we'll import create from Zustand, which will allow us to create a Zustand hook that can be passed in a context. We'll also import the Cart type from @/api/types:
"use client";
import { useState, createContext, useContext } from "react";
import { create } from "zustand";
import { Cart } from "@/api/types";
Remember, we can't have a global variable for our state so we will write a createStore function to create a hook on-the-fly every time we render a new layout.
Inside the function we will call create with the the schema that includes cart and setCart. Then we'll give it the initial cart value and supply a setCart that will set the value with the cart:
const createStore = (cart: Cart) =>
	create<{
		cart: Cart;
		setCart: (cart: Cart) => void;
	}>((set) => ({
		cart,
		setCart(cart: Cart) {
			set({ cart });
		},
	}));
One of the nice things about Zustand is it's pretty simple to implement a store!
Still inside of CartProvider.tsx, we'll create the CartContext.
This will be either the output of createStore or null as it's initialized:
const CartContext = createContext<ReturnType<typeof createStore> | null>(null);
Next, we'll create a custom hook called useCart that will get the Zustand hook by calling useContext passing in the CartContext. The hook will throw an error if it doesn't find a context:
export const useCart = () => {
	if (!CartContext)
		throw new Error("useCart must be used within a CartProvider");
	return useContext(CartContext)!;
};
Finally, we'll create the CartProvider that will take the initial cart and use the createStore function to initialize some state that we'll then pass down to any children using the CartContext.Provider:
const CartProvider = ({
	cart,
	children,
}: {
	cart: Cart;
	children: React.ReactNode;
}) => {
	const [store] = useState(() => createStore(cart));
	return <CartContext.Provider value={store}>{children}</CartContext.Provider>;
};
export default CartProvider;
Updating Components to use CartProvider
Similar to the process we followed before, we need to update our components to use the CartProvider.
Over in layout.tsx, we'll import the CartProvider which will wrap our components:
import CartProvider from "./store/CartProvider";
Everything else in the layout will still work the same way! We're just changing the CartProvider implementation.
Updating the Header
Inside of Header.tsx, start by importing the useCart hook from the CartProvider. Then inside of the component, create a new cartHook variable that is the output of useCart. From there, we'll get the cart by calling the the cartHook with the selector:
// inside Header component
const cartHook = useCart();
const cart = cartHook((state) => state.cart);
As seen in the resources in the introduction, an alternative way to achieve the same result looks like this:
const cart = useCart()((state) => state.cart);
Updating the Cart Popup
Now over in CartPopup.tsx we'll get the whole store including the cart and setCart:
// inside CartPopup.tsx
import { useCart } from "../store/CartProvider";
export default function CartPopup({
  clearCartAction,
}: {
  clearCartAction: () => Promise<Cart>;
}) {
  const { cart, setCart } = useCart()();
  ...
Updating Add to Cart
Finally, over in AddToCart.tsx we'll bring in the useCart hook then use it to get setCart:
// inside AddToCart.tsx
import { useCart } from "../store/CartProvider";
export default function AddToCart({
  addToCartAction,
}: {
  addToCartAction: () => Promise<Cart>;
}) {
  const setCart = useCart()((state) => state.setCart);
  ...
Now with these changes, we can double check our work.
Over in the browser, we can see the initial cart data has the 2 items in it, and we can add items to the cart as expected.
With the cart context all set up, we can move on to setting up the reviews context.
Implementing Review Context
Similar to before, we'll create a new ReviewsProvider.tsx file in the app/store directory. Import the necessary functions for React context, as well as create from Zustand. We'll also import the Review type from @/api/types.
We'll create a createStore function that will give us back a Zustand hook with an array of reviews, as well as a setReviews function that will set the value with the reviews:
"use client";
import { useState, createContext, useContext } from "react";
import { create } from "zustand";
import { Review } from "@/api/types";
const createStore = (reviews: Review[]) =>
	create<{
		reviews: Review[];
		setReviews: (Reviews: Review[]) => void;
	}>((set) => ({
		reviews,
		setReviews(reviews: Review[]) {
			set({ reviews });
		},
	}));
Next, we'll create our ReviewsContext then create a custom hook called useReviews that will give us the hook we can use to fetch reviews from the Zustand store. If it doesn't find a context, it will throw an error:
const ReviewsContext = createContext<ReturnType<typeof createStore>>(null!);
export const useReviews = () => {
	if (!ReviewsContext)
		throw new Error("useCart must be used within a CartProvider");
	return useContext(ReviewsContext);
};
Finally, we'll create the ReviewsProvider. Like before, we will use the useState hook to hold the state of the store:
const ReviewsProvider = ({
	reviews,
	children,
}: {
	reviews: Review[];
	children: React.ReactNode;
}) => {
	const [store] = useState(() => createStore(reviews));
	return (
		<ReviewsContext.Provider value={store}>{children}</ReviewsContext.Provider>
	);
};
export default ReviewsProvider;
With the reviews context created, we can update our components to use it.
Updating Components for the Reviews Context
At the top of pages.tsx we can import the ReviewsProvider, then wrap the entire tree with it:
import ReviewsProvider from "@/app/store/ReviewsProvider";
...
// inside the ProductDetail return
 return (
    <ReviewsProvider reviews={product.reviews}>
      <div className="flex flex-wrap">
      ...
Over in AverageRating.tsx, we'll import the useReviews hook and use it to fetch the reviews state instead of using the reviews prop:
import { useReviews } from "@/app/store/ReviewsProvider";
export default function AverageRating() {
  const reviews = useReviews()((state) => state.reviews);
  ...
Remember, when we use the Zustand hook we call it then give it a function that will take the state and return the value we want.
We need to follow a similar process in Reviews.tsx. Import the useReviews hook, and remove the reviews prop from the AverageRating component:
// inside Reviews.tsx
const { reviews, setReviews } = useReviews()();
Now we can double check our work!
Checking Our Work
Back in the browser, we can see the reviews are being fetched from the server and displayed as expected in both the UI and the rendered source. We can also add reviews and see them appear in the list.
Now that you've seen how simple Zustand is, you can see why it's a popular choice for state management in React apps.
Next up, we'll re-implement this functionality with the Jotai library, which follows the atomic model for state management.