Back to roadmaps zustand Course

Project: E-commerce Shopping Cart Store

In this project, we will build a shopping cart store. The store manages an array list of items, calculates total prices, handles unit increments or removals, and persists the cart list across browser sessions.


1. Cart Store Specifications

  • Add Product: If the item exists in the cart, increment its quantity. If not, append it with a quantity of 1.
  • Modify Quantity: Increment or decrement quantities, removing the item entirely if quantity drops to 0.
  • Totals Calculations: Dynamically computes subtotal and discount rates.
  • Persistence: Synchronizes state automatically using the persist middleware.

2. Implementing the Cart Store

Create a store file named useCartStore.ts inside src/stores/:

// src/stores/useCartStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

export interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  cart: CartItem[];
  addToCart: (product: Omit<CartItem, "quantity">) => void;
  updateQuantity: (id: number, amount: number) => void;
  removeFromCart: (id: number) => void;
  clearCart: () => void;
  getTotals: () => { subtotal: number; itemsCount: number };
}

export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      cart: [],

      addToCart: (product) => set((state) => {
        const existingItem = state.cart.find((item) => item.id === product.id);
        
        if (existingItem) {
          return {
            cart: state.cart.map((item) =>
              item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
            ),
          };
        }
        
        return { cart: [...state.cart, { ...product, quantity: 1 }] };
      }),

      updateQuantity: (id, amount) => set((state) => ({
        cart: state.cart
          .map((item) =>
            item.id === id ? { ...item, quantity: item.quantity + amount } : item
          )
          .filter((item) => item.quantity > 0),
      })),

      removeFromCart: (id) => set((state) => ({
        cart: state.cart.filter((item) => item.id !== id),
      })),

      clearCart: () => set({ cart: [] }),

      getTotals: () => {
        const cart = get().cart;
        const subtotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
        const itemsCount = cart.reduce((sum, item) => sum + item.quantity, 0);
        return { subtotal, itemsCount };
      },
    }),
    {
      name: "shopping-cart-storage", // local storage key
    }
  )
);

3. Implementing the Cart Interface

Here is the React cart component:

// src/components/ShoppingCart.tsx
import { useCartStore } from "../stores/useCartStore";

export default function ShoppingCart() {
  const { cart, updateQuantity, removeFromCart, clearCart, getTotals } = useCartStore();
  const { subtotal, itemsCount } = getTotals();

  return (
    <div className="max-w-xl mx-auto p-6 bg-white border rounded-xl shadow-sm mt-8">
      <h2 className="text-2xl font-bold">Shopping Basket</h2>
      <p className="text-gray-500 text-sm mt-1">{itemsCount} items in basket</p>

      {cart.length === 0 ? (
        <p className="text-gray-400 text-center py-8">Your cart is empty.</p>
      ) : (
        <div className="mt-6 divide-y space-y-4">
          {cart.map((item) => (
            <div key={item.id} className="flex justify-between items-center py-4">
              <div>
                <h4 className="font-semibold text-gray-950">{item.name}</h4>
                <p className="text-gray-500 text-sm">${item.price} each</p>
              </div>
              
              <div className="flex items-center gap-3">
                {/* Decrease button */}
                <button 
                  onClick={() => updateQuantity(item.id, -1)}
                  className="w-8 h-8 border rounded flex items-center justify-center hover:bg-gray-50"
                >
                  -
                </button>
                <span className="font-semibold">{item.quantity}</span>
                {/* Increase button */}
                <button 
                  onClick={() => updateQuantity(item.id, 1)}
                  className="w-8 h-8 border rounded flex items-center justify-center hover:bg-gray-50"
                >
                  +
                </button>
                {/* Delete button */}
                <button 
                  onClick={() => removeFromCart(item.id)}
                  className="text-red-500 ml-4 hover:underline"
                >
                  Remove
                </button>
              </div>
            </div>
          ))}

          {/* Subtotal and Summary details */}
          <div className="pt-6 space-y-2">
            <div className="flex justify-between font-bold text-lg">
              <span>Subtotal:</span>
              <span>${subtotal}</span>
            </div>
            <div className="flex gap-4 pt-4">
              <button 
                onClick={clearCart}
                className="w-1/2 border py-2.5 rounded-lg text-gray-600 hover:bg-gray-50"
              >
                Clear Cart
              </button>
              <button className="w-1/2 bg-blue-600 text-white py-2.5 rounded-lg hover:bg-blue-700">
                Proceed to Checkout
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
Published on Last updated: