E-commerce Store – The Most Performant Site I’ve Ever Built

E-commerce Store – The Most Performant Site I’ve Ever Built

E-commerce Store – The Most Performant Site I’ve Ever Built

JavaScript – React – REST API – TailwindCSS – Vite

This is an e-commerce store built with the FakeStore API using React, React Query, TailWind CSS, Vite and Netlify for CI/CD.

lighthouse score

Challenge 1

Every time a user added an item to their cart, it triggered re-renders all the way up the component tree, including components that didn’t need to update. This led to performance issues and inefficient rendering across the app.

Before : Profile data of an “Add to cart” click

Approach

To solve this, I modularized the cart functionality by:

Moving all cart logic (state + actions) into a custom hook:
This included cart, addToCart, removeFromCart, totalItems, and totalPrice. The hook handles localStorage persistence and ensures cart logic is completely isolated from the App component.

Memoize return object

useMemo now stabilizes the return object

CartProvider’s context value doesn’t change unless something actually changed

addToCart function no longer changes reference

import { useState, useEffect, useMemo, useCallback } from 'react';

const CART_KEY = 'user-cart';

// Initialize cart state from local storage for persistence across page reloads
export default function useCart(){
    const [cart, setCart] = useState(() => {
      const saved = localStorage.getItem(CART_KEY);
      try {
        return saved ? JSON.parse(saved) : [];
      } catch {
        localStorage.removeItem(CART_KEY);
        return [];
      }
    });
  
    // Sync cart state to localStorage whenever it changes
    useEffect(() => {
    localStorage.setItem(CART_KEY, JSON.stringify(cart));
  }, [cart]);

    // Add product to cart or increase quantity if it already exists
    const addToCart = useCallback((product) => {
      setCart((prevCart) => {
        const updatedCart = prevCart.reduce((acc, item) => {
          if (item.id === product.id) {
            acc.push({ ...item, quantity: item.quantity + 1 });
          } else {
            acc.push(item);
          }
          return acc;
        }, []);
        ...

// MEMOIZE THE RETURN OBJECT
  return useMemo(() => ({
    cart,
    addToCart,
    removeFromCart,
    totalItems,
    totalPrice,
  }), [cart, addToCart, removeFromCart, totalItems, totalPrice]);

Separating Cart Context into two parts:

CartActionsContext: holds non-changing functions like addToCart and removeFromCart

CartStateContext: holds reactive values like cart, totalItems, and totalPrice

import { createContext, useContext, useMemo } from 'react';
import useCart from '../hooks/useCart';

const CartStateContext = createContext();
const CartActionsContext = createContext();

export function CartProvider({ children }) {
  const { cart, totalItems, totalPrice, addToCart, removeFromCart } = useCart();

    const stateValue = useMemo(() => ({
    cart,
    totalItems,
    totalPrice
  }), [cart, totalItems, totalPrice]);

  const actionsValue = useMemo(() => ({
    addToCart,
    removeFromCart
  }), [addToCart, removeFromCart]);

  return (
    <CartStateContext.Provider value={stateValue}>
      <CartActionsContext.Provider value={actionsValue}>
        {children}
      </CartActionsContext.Provider>
    </CartStateContext.Provider>
  );
}

export const useCartState = () => useContext(CartStateContext);
export const useCartActions = () => useContext(CartActionsContext);

Using the context selectively at the component level:

Instead of passing cart-related props through multiple layers, components now consume only the data or actions they need, directly from context.

import { useCartActions, useCartState } from "../context/CartContext";

const SideCart = () => {
    const { addToCart, removeFromCart } = useCartActions();
    const { cart, totalPrice, totalItems } = useCartState();
    ...

Result

By:

  • Extracting cart logic into a custom hook
  • Splitting state and actions into separate contexts
  • Consuming them only where needed

I was able to significantly reduce unnecessary re-renders across the app. Components like SingleProduct, Hero, and ProductRow now stay completely untouched when the cart updates, while components like SideCart and Navbar re-render appropriately.

This approach not only improved performance, but also made the cart functionality more modular, testable, and easier to maintain.

After : Profile data of an “Add to cart” click

Challenge 2

To improve optimization and performance, I leveraged the useQuery hook to cache all products retrieved from the API. This approach worked well since the dataset was static and rarely changed.

Approach

To make the solution scalable, I integrated React Query (TanStack Query) with a stale and cache time set to one week. This ensures efficient data retrieval by storing the results locally, minimizing unnecessary API calls.

  const { data: products, isLoading: loading, error } = useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
    staleTime: 1000 * 60 * 60 * 24 * 7, // 1 week
    cacheTime: 1000 * 60 * 60 * 24 * 7,  // Cached for a week even if no components are using
  });

Result

This approach significantly reduced unnecessary API requests, improved load times, and provided a smoother user experience. By caching the product data for one week, the app delivered faster performance while maintaining up to date content with minimal overhead.

View All Projects