DEV Community

Cover image for Manage Cart State with Zustand
Abhirup Datta
Abhirup Datta

Posted on

Manage Cart State with Zustand

Zustand, meaning “state” in German, is a lightweight state management library for React that offers an intuitive and efficient way to manage application state.

In this blog, I will setup a simple cart application in Zustand .
In Zustand, we store the state in a hook rather than a context (used in React-redux).

Let's see the code 😄
Note: I have used TailwindCSS for styling the UI.

After finishing, this is we will get this:
Output

App.tsx

// Example static food items
import FoodItems from './FoodItems';
import ShoppingCart from './ShoppingCart';
import {Food} from './types';

const foodItems:Food[] = [
  {
    id: 1,
    name: "Burger",
    description: "Juicy grilled beef burger with fresh veggies",
    price: 5.99,
    image: "https://images.unsplash.com/photo-1568901346375-23c9450c58cd?q=80&w=2799&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    id: 2,
    name: "Pizza",
    description: "Cheesy pizza topped with fresh tomatoes and basil",
    price: 8.99,
    image: "https://images.unsplash.com/photo-1513104890138-7c749659a591?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    id: 3,
    name: "Salad",
    description: "Fresh mixed greens with a tangy vinaigrette",
    price: 4.99,
    image: "https://plus.unsplash.com/premium_photo-1701699257759-4189d01f4351?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    id: 4,
    name: "Pasta",
    description: "Creamy Alfredo pasta with grilled chicken",
    price: 7.99,
    image: "https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?q=80&w=2706&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
];
const App = ()=> {

  return (
    <div className="flex">
        <ShoppingCart />
        <FoodItems items={foodItems}/>
    </div>
  )
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Here, I have added some sample food items. This is passed as props to FoodItems.We will see how to add the items from FoodItems to ShoppingCart "magically" via Zustand.

cartStore.ts

import { Food } from '@/types';
import {create} from 'zustand';

type Sku = {
    item: Food,
    count: number,
}
type CartStoreState = { 
    cart: Sku[],
    addToCart: (food: Food)=> void,
    incrementDecrementSku: (foodId: number, type: 'increment'|'decrement', quantity?: number )=> void,
    deleteSku: (foodId: number )=> void,
    clearCart: ()=> void,
}


export const useCartStore = create<CartStoreState>((set)=>({
    cart: [],
    addToCart: (product)=> set((state)=> {
        const cart = state.cart;
        const isSameProductExists = cart.find(sku => sku.item.id === product.id);
        if(isSameProductExists){
            isSameProductExists.count += 1;
            return {cart:[...cart]};
        }
        return {cart: [...state.cart, {item: product, count: 1}] }
    }) ,
    incrementDecrementSku:(productId,  type, quantity = 1)=> set(state=> {
        const cart = state.cart;
        const productIndex = cart.findIndex(sku => sku.item.id === productId);
        if(productIndex !== -1){
            const updatedCart = [...cart];
            const currentSku = updatedCart[productIndex];

            if(type === 'decrement'){
                if(currentSku.count <= quantity){
                   updatedCart.splice(productIndex,1); // Remove the product if count reaches 0 or below
                }else{
                    currentSku.count -= quantity;
                }
            }else if(type === 'increment'){
                currentSku.count += quantity;
            }
            return {cart:updatedCart};
        }
        return {cart: cart }
    }),
    deleteSku:(id)=> set((state)=> ({cart: state.cart.filter(sku=> sku.item.id !== id)})),
    clearCart:()=> set({cart: []})
}));

Enter fullscreen mode Exit fullscreen mode

The create function from zustand helps in creating a store .It takes a callback function with the first param which is a setter method 'set' and returns the state.
Here, the state has five items: cart, addToCart, incrementDecrementSku,deleteSku , clearCart .

FoodItems.tsx

import { useCartStore } from './store/cartStore';
import {Food} from './types';
import { Plus } from "lucide-react";

const FoodItems = ({ items }:{items: Food[]}) => {
    const {addToCart} = useCartStore();

    return (
        <div className="w-3/4 p-4">
          <h1 className="text-2xl font-bold mb-4 text-center">Food Cart</h1>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
            {items.map((item) => (
              <div
                key={item.name}
                className="border p-4 rounded-lg shadow-md flex flex-col items-center"
              >
                <img
                  src={item.image}
                  alt={item.name}
                  className="w-32 h-32 object-cover mb-4 rounded"
                />
                <h2 className="text-lg font-semibold mb-2">{item.name}</h2>
                <p className="text-gray-600 mb-2">{item.description}</p>
                <p className="text-green-500 font-bold mb-2">${item.price}</p>
                <button className="p-2 rounded-full bg-blue-500 text-white hover:bg-blue-600" onClick={()=>addToCart(item)}>
                  <Plus className="w-5 h-5" />
                </button>
              </div>
            ))}
          </div>
      </div>
    );
  };

export default FoodItems;
Enter fullscreen mode Exit fullscreen mode

The FoodItems displays the item by looping over them and each item has a add button which calls the addToCart function of useCartStore.

ShoppingCart.tsx

import { Minus, Plus, ShoppingCart as ShoppingCartIcon } from "lucide-react";
import { useCartStore } from "./store/cartStore";

const ShoppingCart = ()=>{

  const {cart, incrementDecrementSku, clearCart, deleteSku} = useCartStore();

    return (<div className="w-1/4 p-4 bg-gray-100 shadow-md">
          <div className="flex flex-col items-center">
            <button className="p-4 rounded-full bg-gray-200 hover:bg-gray-300">
              <ShoppingCartIcon className="w-8 h-8 text-gray-700" />
            </button>
            <button className="p-2 rounded-full bg-gray-200 hover:bg-gray-300 mt-1" onClick={clearCart}>
              Clear All
            </button>
            <p className="mt-2 text-gray-700 font-semibold">Your Cart</p>
            {
              cart.map((sku)=>(
                <div key={sku.item.id}  className="bg-blue-200 p-2 px-3 rounded-lg m-2 min-w-full">
                  <div className="flex items-center justify-between gap-2">
                    {sku.item.name}
                    <QuantityControlButton count={sku.count} 
                    decrementBy1={()=> incrementDecrementSku(sku.item.id,'decrement')}
                    incrementBy1={()=> incrementDecrementSku(sku.item.id,'increment')}
                    />
                  </div>
                  <button className="border border-red-500 p-1 rounded-md" onClick={()=> deleteSku(sku.item.id)}>Delete</button>
                </div>
              ))
            }
          </div>
        </div>)
}

export default ShoppingCart;

type QuantityControlButtonProps = {
  count: number;
  incrementBy1: ()=> void;
  decrementBy1: ()=> void;

}
export const QuantityControlButton = ({count, decrementBy1, incrementBy1}:QuantityControlButtonProps)=>{
  return (
    <div className="flex items-center justify-between border-2 gap-4 border-yellow-500 p-1 rounded-3xl">
     <Minus size={16} onClick={decrementBy1} className="cursor-pointer"></Minus>
     {count}
     <Plus size={16} onClick={incrementBy1} className="cursor-pointer"></Plus>

    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

In the ShoppingCart, we are adding/removing/deleting items from the cart.

Overall, Zustand helps us to write state management code in a leaner way compared to React-redux.

Top comments (0)