typescriptUpdated about 2 months ago

React Context example for e-commerce cart system

by @Apiwit

typescript
"use client";

import { ProductInterface } from "@/interfaces/product";
import { useRouter } from "next/navigation";
import {
  createContext,
  useContext,
  useEffect,
  useState,
  useCallback,
  ReactNode,
  useMemo,
} from "react";
import { toast } from "sonner";

type CartItem = ProductInterface;

interface CartContextType {
  cartItems: CartItem[];
  cartLength: number;
  total: number;
  totalCheckout: number;
  checkoutItems: CartItem[]; // for single product instant checkout
  buyNow: (item: CartItem | CartItem[]) => void;
  addToCart: (item: CartItem) => void;
  removeFromCart: (id: string) => void;
  clearCheckoutItems: (items?: CartItem[]) => void;
}

const CartContext = createContext<CartContextType | undefined>(undefined);

const CART_KEY = "cart";

export const CartProvider = ({ children }: { children: ReactNode }) => {
  const router = useRouter();

  const [cartItems, setCartItems] = useState<CartItem[]>([]);
  const [checkoutItems, setCheckoutItems] = useState<CartItem[]>([]);

  const total = useMemo(() => {
    return cartItems.reduce((acc, curr) => acc + curr.price, 0);
  }, [cartItems]);

  const totalCheckout = useMemo(() => {
    return checkoutItems.reduce((acc, curr) => acc + curr.price, 0);
  }, [checkoutItems]);

  useEffect(() => {
    if (typeof window !== "undefined") {
      const localCarts = localStorage.getItem(CART_KEY);
      const carts = localCarts ? JSON.parse(localCarts) : [];

      if (!Array.isArray(carts)) {
        toast.error(
          "Encountered invalid data. The cart will be cleared. Please add item to the cart again."
        );
        localStorage.removeItem(CART_KEY);
        setCartItems([]);
      } else {
        setCartItems(carts);
      }
    }
  }, []);

  const buyNow = useCallback(
    (item: CartItem | CartItem[]) => {
      if (!Array.isArray(item)) {
        setCheckoutItems([item]);
      } else {
        setCheckoutItems(item);
      }

      router.push("/checkout");
    },
    [router]
  );

  const addToCart = useCallback((newItem: CartItem) => {
    setCartItems((prev) => {
      const isDuplicate = prev.some((item) => item.id === newItem.id);

      if (isDuplicate) {
        toast.info("Item already in cart.");
        return prev;
      }

      const updated = [...prev, newItem];
      localStorage.setItem(CART_KEY, JSON.stringify(updated));
      toast.success("Item added to cart.");
      return updated;
    });
  }, []);

  const removeFromCart = useCallback((id: string) => {
    setCartItems((prev) => {
      const updated = prev.filter((item) => item.id !== id);
      localStorage.setItem(CART_KEY, JSON.stringify(updated));
      return updated;
    });
  }, []);

  const clearCheckoutItems = useCallback((newItems?: CartItem[]) => {
    if (newItems?.length) {
      setCartItems((prev) => {
        const updated = prev.filter(
          (item) => !newItems.some((item2) => item2.id === item.id)
        );
        localStorage.setItem(CART_KEY, JSON.stringify(updated));
        return updated;
      });
    } else {
      setCartItems([]);
      localStorage.setItem(CART_KEY, JSON.stringify([]));
    }

    setCheckoutItems([]);
  }, []);

  const value = {
    cartItems,
    cartLength: cartItems.length,
    total,
    totalCheckout,
    checkoutItems,
    buyNow,
    addToCart,
    removeFromCart,
    clearCheckoutItems,
  };

  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};

export const useCart = () => {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error("useCart must be used within a CartProvider");
  }
  return context;
};

Description

An example of e-commerce cart system using context. Have common action such as add, edit or view cart items.

Comments