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.