Add to Cart Logic With Zustand state management library

Add to Cart Logic With Zustand state management library

Hi There , today , we are going to implement a shopping cart button using React and Zustand state management library.

We will discover :

  • Fetch and display products from external API

  • Create reusable products List component

  • Create Resusable Add to Cart Button

  • Create the application Store to handle added products to cart

  • Update the products count state on the shopping cart icon on add to cart / remove from event

You can Find the complete source code on Github :

https://github.com/chikno/whitebeard-dev/tree/zustand-intro-add-to-cart

Let's discover our folder architecture :

Let's go.

Install dependencies


pnpm add axios @tanstack/react-query zustand

Create the API provider

create a file api-provider.tsx in your app (inside API folder) and fill it with this content


import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import * as React from 'react';

export const queryClient = new QueryClient();

export function APIProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

The next step is to wrap our app component with the query client provider.

Open main.tsx file and update it like this

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { APIProvider } from './common/api-provider';
const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <APIProvider>
      <App />
    </APIProvider>
  </React.StrictMode>
);

Create the Axios Client

Let's create our axios client once . then we will use it to send every request to the API. it will prevents us to write each time the request options.

create a file called : client.js , then put this code on it


import axios from 'axios';
export const client = axios.create({
  baseURL: "https://dummyjson.com/"
});

Products Type

Create a products type to safely insure that we send and get the correct data through our api calls.

Create a file called products.ts inside a types folder.

export type AddtoCartStatus = {
  count: number;
  products: Product[];
  addToCart: (product: Product) => void;
  removeFromCart: (id: number) => void;
};

export type Product = {
  id: number;
  title: string;
  description: string;
  category: string;
  brand: string;
  price: number;
  discountPercentage: number;
  availabilityStatus: string;
  rating: number;
  reviews: Review[];
  stock: number;
  dimensions: Dimensions;
  images: string[];
  meta: Meta;
  minimumOrderQuantity: number;
  returnPolicy: string;
  shippingInformation: string;
  sku: string;
  tags: string[];
  thumbnail: string;
  warrantyInformation: string;
  weight: number;
};

type Review = {
  comment: string;
  date: string;
  rating: number;
  reviewerEmail: string;
  reviewerName: string;
};

type Dimensions = {
  width: number;
  height: number;
  depth: number;
};

type Meta = {
  createdAt: string;
  updatedAt: string;
  barcode: string;
  qrCode: string;
};

Implement the custom hook to fetch data

Create a file called use-products.ts

import type { AxiosError , AxiosResponse} from "axios";
import { createQuery } from "react-query-kit";

import { client } from "../common/client";
import type { Product } from "../types/product";

type Response = Product[];
type Variables = undefined;

export const useProducts = createQuery<Response, Variables, AxiosError>({
  queryKey: ["products"],
  fetcher: () => {
    return client.get(`/products`).then((response: AxiosResponse<Response>) => response.data?.products);
  },
});

in this Hook, we will use our client to fetch Dummy products from the API.

The store

Before diving into the display , we need to create our store , so we can store our products added to cart ,then share this data throught our app components

create a file store.ts in the project root and fill it with the code below

import { create } from "zustand";
import { AddtoCartStatus, Product } from "./types/product";
export const useAddtoCart = create<AddtoCartStatus>((set) => ({
  count: 0,
  products: [],
  addToCart: (product: Product) =>
    set((state) => ({
      count: state.count + 1,
      products: [...state.products, product],
    })),
  removeFromCart: (id: number) =>
    set((state) => ({
      count: state.count - 1,
      products: state.products.filter((_product) => _product.id !== id),
    })),
}));

in this store , we call the "create" method from zustand then we populate it with our data model for the add to cart logic.

our data model will hold those data :

  • count : initial number of products in the cart

  • products : initial products in the cart

  • addToCart method : we call the built-in zustand set method to set the count and the product added to cart,

  • removeFromCart : we call the built-in zustand set method to set the count and the product added to cart,

The add to cart button

Create a file add-to-cart-button inside of the components folder ,

import React from 'react'
import { Product } from '../types/product'
import { ShoppingCart } from '../icons/shoping-cart';
import { useAddtoCart } from '../store';

type Props = {
  product: Product;
  removeProductFromCart: (id: number) => void;
  addProductToCart: (product: Product) => void;
}
const AddtoCartButton = ({ product, removeProductFromCart, addProductToCart }: Props) => {

  const products = useAddtoCart((state) => state.products);
  const [isProductInCart, setisProductInCart] = React.useState(false);

  React.useEffect(() => {
    setisProductInCart(products.includes(product));
  }, [products])

  return (
    <>
      {!isProductInCart ? <button onClick={() => {
        addProductToCart(product)
      }} className="flex items-center justify-center rounded-md bg-slate-900 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-blue-300">
        <ShoppingCart />
        Add to cart
      </button> : <button onClick={() => {
        removeProductFromCart(product?.id)
      }} className="flex items-center justify-center rounded-md bg-slate-900 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-blue-300">
        Remove from cart
      </button>}

    </>
  )
}

export default AddtoCartButton

The Product Card

then let's create the product Card component , this component will display a single product Card.

import React from 'react'
import { Product } from '../types/product'
import { Rating } from './rating'
import { useAddtoCart } from '../store';
import AddtoCartButton from './add-to-cart-button';

type Props = {
    product: Product,
}

export const Card = ({ product }: Props) => {
    const addToCart = useAddtoCart((state) => state.addToCart);
    const removeFromCart = useAddtoCart((state) => state.removeFromCart);
    const addProductToCart = (product:Product) => {
        if (!product) return;
        addToCart(product);
    }

    const removeProductFromCart = (id: number) => {
        if (!id) return;
        removeFromCart(id);
    }

    return (
        <>
            <a className="relative mx-3 mt-3 flex overflow-hidden rounded-xl" href="#">
                <img className="object-cover" src="https://images.unsplash.com/photo-1600185365483-26d7a4cc7519?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8OHx8c25lYWtlcnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60" alt="product image" />
                <span className="absolute top-0 left-0 m-2 rounded-full bg-black px-2 text-center text-sm font-medium text-white">-{product?.discountPercentage} off</span>
            </a>
            <div className="mt-4 px-5 pb-5">
                <a href="#">
                    <h5 className="text-xl tracking-tight text-slate-900">{product?.title}</h5>
                </a>
                <div className="mt-2 mb-5 flex items-center justify-between">
                    <p>
                        <span className="text-3xl font-bold text-slate-900">{product?.price}</span>
                    </p>
                    <div className="flex items-center">
                        <Rating rating={product?.rating} />
                        <span className="mr-2 ml-3 rounded bg-yellow-200 px-2.5 py-0.5 text-xs font-semibold">{product?.rating}</span>
                    </div>
                </div>

                <AddtoCartButton product={product} addProductToCart={addProductToCart} removeProductFromCart={removeProductFromCart} />
            </div>
        </>

    )
}

the magic happens here

    const addToCart = useAddtoCart((state) => state.addToCart);

to add a product to the card , we simply call our method from store "addToCart" then we pass the product as a param.

then in the sticky-navbar.tsx component wich contains the shoping cart icon , we bind the products count also from the store

    const count = useAddtoCart((state) => state.count)

then Zustand will do the JOB for you. Awesome. isn'it ?

The Products List

inside the component folder let's create a component called list.tsx , this component will receive the products array as a prop and will display it.

import React from 'react'
import { Product } from '../types/product';
import { Card } from './card';


type Props = {
    products: Product[];
}
export const List = ({ products }: Props) => {

    const renderItem = React.useCallback(
        ({ product }: { product: Product }) => {
            return (
                <div >
                    <Card key={product.id}  product={product} />,
                </div>
            )
        }, []
    );

    return (
        <>
            {products && products.map((product: Product) => {
                return renderItem({ product })
            })}
        </>
    )
}

That's it.

Happy Coding.