Building a Scalable Shopping Cart System with React UserReducer and ContextAPI

Shopping carts are a common feature in e-commerce websites, and implementing them can be a challenging task for beginner developers. However, with the help of React's useReducer and ContextAPI, it becomes a lot easier to create a reusable and efficient shopping system. In this article, we'll learn how to build a shopping cart from scratch using the two powerful tools.

NOTE: : You must have a basic understanding of ContextAPI and React's useReducer hooks in order to fully comprehend this article. For ContextAPI and useReducer, I've previously written a blog article. I'll be utilizing TailwindCSS for styling because it produces clean, quick CSS.

Step 1: Setting up the React project

To start, we need to create a new React project. you can use any preferred method for setting up a React project, including using a code editor or creating a project with create-react-app. I will be using Vite for development.

Step 2: Creating the Product Component

Next, we'll create the Product component that will represent each item in our shopping cart. It will contain the product name, price, image, and a button to add the item to the cart.

const Product => (props) {
  const { name, image, price, id } = props.product;

  const handleAddToCart = () => {
    console.log("product added in cart")
  };

  return (
    <div className="">
      <img className="h-[300px] w-full rounded" src={image} alt={name} />
      <div className="flex justify-between items-center w-full py-3">
        <h3 className="font-semibold">{name}</h3>
        <p className="text-gray-500 font-semibold">${price}</p>
      </div>
      <button
        className="bg-yellow-500 text-white py-2 px-4 rounded-md w-full"
        onClick={handleAddToCart}
      >
        Add to Cart
      </button>
    </div>
  );
};

export default Product;

Step 3: Creating the Cart Component

In this step, we'll use ContextAPI to manage the state of our shopping cart. The context will provide the current state of the cart and the methods to add or remove items from it.

If you are unfamiliar with useReducer and ContexAPI, the code following may be difficult to understand.

import React, { createContext, useReducer } from "react";

const initialState = {
  items: [],
  total: 0,
};

const cartReducer = (state, action) => {
  switch (action.type) {
    case "ADD_ITEM":
      return {
        ...state,
        items: [...state.items, action.item],
        total: state.total + action.item.price,
      };
    case "REMOVE_ITEM":
      return {
        ...state,
        items: state.items.filter((item) => item.id !== action.id),
        total: state.total - action.price,
      };
    case "RESET":
      return {
        items: [],
        total: 0,
      };
    default:
      return state;
  }
};

export const CartContext = createContext({
  cart: initialState,
  addItem: () => {},
  removeItem: () => {},
});

export const CartProvider = ({ children }) => {
  const [cart, dispatch] = useReducer(cartReducer, initialState);

  const addItem = (item) => {
    dispatch({ type: "ADD_ITEM", item });
  };

  const removeItem = (id, price) => {
    dispatch({ type: "REMOVE_ITEM", id, price });
  };

  const reset = () => {
    dispatch({ type: "RESET" });
  };

  return (
    <CartContext.Provider value={{ cart, addItem, removeItem, reset }}>
      {children}
    </CartContext.Provider>
  );
};

Step 4: Using the Cart Context in the product Component

Now that we have created the Cart Context, we can use it in the Product component to add the item to the cart when the add to cart button is clicked.

import React, { useContext } from "react";
import { CartContext } from "./CartContext";

const Product = (props) => {
  const { name, image, price, id } = props.product;

  const { addItem } = useContext(CartContext);

  const handleAddToCart = () => {
    addItem({ name, price, id, image });
  };

  return (
    <div className="shadow-md">
      <img className="h-[300px] w-full rounded" src={image} alt={name} />
      <div className="flex justify-between items-center w-full py-3">
        <h3 className="font-semibold">{name}</h3>
        <p className="text-gray-500 font-semibold">${price}</p>
      </div>
      <button
        className="bg-yellow-500 text-white py-2 px-4 rounded-md w-full"
        onClick={handleAddToCart}
      >
        Add to Cart
      </button>
    </div>
  );
};

export default Product;

Step 5: Creating the Cart Component

Now, we'll create the Cart Component to display the items and total amount in the shopping cart.

import React, { useContext } from "react";
import { CartContext } from "./CartContext";
import trash from "./assets/delete.svg";

const Cart = () => {
  const { cart, removeItem, reset } = useContext(CartContext);
  const itemsCount = {};

  cart.items.forEach((item) => {
    if (itemsCount[item.id]) {
      itemsCount[item.id].count += 1;
    } else {
      itemsCount[item.id] = { ...item, count: 1 };
    }
  });

  return (
    <div className="absolute z-[100] w-80 top-0 right-0 bg-white overflow-hidden shadow-xl rounded-md">
      <h2 className="border-b p-5 font-semibold">Cart</h2>
      {cart.items.length != 0 ? (
        <div className="flex flex-col space-y-4">
          {Object.values(itemsCount).map((item) => (
            <div
              className="flex justify-between pt-4 items-center px-5"
              key={item.id}
            >
              <div className=" rounded-md overflow-hidden">
                <img className="h-10 w-10 object-fit" src={item.image} alt="" />
              </div>
              <p className="text-sm text-gray-600">{item.name}</p>
              <p className="text-sm text-gray-500">
                {`$${item.price} X ${item.count}`}
              </p>
              <img
                className="h-4 w-4 object-fit cursor-pointer"
                src={trash}
                alt=""
                onClick={() => removeItem(item.id, item.price)}
              />
            </div>
          ))}
          <div className="flex justify-between items-center px-4">
            <button
              className="rounded-lg bg-gray-700 text-white px-4 py-1"
              onClick={reset}
            >
              Reset
            </button>
            <h3 className="border-t p-5 text-right font-extrabold">
              Total: {cart.total}
            </h3>
          </div>
        </div>
      ) : (
        <div className="flex justify-center items-center w-full px-5 pt-12 pb-8">
          <p className="text-sm font-bold text-gray-500">
            No Item Added to the Cart
          </p>
        </div>
      )}
    </div>
  );
};

export default Cart;

Step 6: Adding Cart component into Header Component

In this step, we'll wrap the App component around the cart component so that it appears at the top and has access to the Cart context, which we'll perform in the following step.

import React, { useContext, useState } from "react";
import cartIcon from "./assets/cart.svg";
import Cart from "./Cart";
import { CartContext } from "./CartContext";

const Header => () {
  const [isOpen, setIsOpen] = useState(false);

  const { cart } = useContext(CartContext);

  function handleclick() {
    setIsOpen((prev) => !prev);
  }
  return (
    <div>
      <div className="flex justify-between items-center mb-8 items-center">
        <h1 className="text-center text-4xl font-semibold">
          Simple Shopping Cart
        </h1>
        <div className="relative">
          <img
            className="cursor-pointer h-6 w-6"
            src={cartIcon}
            onClick={handleclick}
          />
          <div className="absolute bg-orange-800 text-white text-[10px] flex justify-center items-center rounded-full p-1 h-4 w-4 -top-1 -right-1">
            {cart.items.length}
          </div>
        </div>
      </div>
      <div className="relative">{isOpen ? <Cart /> : null}</div>
    </div>
  );
} 

export default Header;

Step 7: Wrapping the App with CartProvider

Finally, we need to wrap our App component with the CartProvider to make the Cart context available throughout the app.

import Product from "./Product";
import { CartProvider } from "./CartContext";
import { products } from "./productData";
import Header from "./Header";

const App = () => {
  return (
    <CartProvider>
      <div className="px-6 py-12 max-w-[1300px] mx-auto">
        <Header />
        <div className="grid md:grid-cols-2 gap-8 md:px-12 lg:grid-cols-3 gap-12">
          {products.map((product) => (
            <Product product={product} key={product.id} />
          ))}
        </div>
      </div>
    </CartProvider>
  );
};

export default App;

And that's it! With these simple steps, you have created a reusable and efficient shopping cart using React's useReducer and ContextAPI.

In conclusion, the combination of useReducer and ContextAPI in React provides a simple and efficient way to manage the state of a shopping cart system. This approach eliminates the need for prop drilling and makes the state of the shopping cart available throughout the app. The CartContext, along with the CartProvider, acts as a centralized store for the shopping cart data, and useReducer is responsible for updating the state based on the actions performed. With these tools, developers can easily create complex e-commerce applications with a scalable and reusable shopping cart system.


2
Subscribe to my newsletter

Read articles from Amit Vishwakarma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Amit Vishwakarma
Amit Vishwakarma

Web dev learner, and anime enthusiast. Building skills & creating websites. Always seeking new anime to add to my watchlist.