AI Scope Digital
HOME//BLOGS//BLOG

Building a Custom Shopping Cart with React

Blog OCT 29, 2026 8 MIN READ

To build a custom shopping cart with React, store cart state in a central place (React Context with useReducer, or a library like Zustand or Redux Toolkit), define actions to add, remove, and update items, derive totals from that state, persist the cart in localStorage or on the server, build reusable cart components (cart icon, drawer, line items, summary), and hand off checkout to a secure payment provider such as Stripe. Prices, discounts, taxes, and inventory must always be validated on the server — never trust the browser. A custom React cart gives you full control over UX, upsells, and integrations that templated stores can't match. Here's a step-by-step guide with code.

When a Custom React Cart Makes Sense

Situation Custom React Cart Hosted Platform Cart (Shopify, WooCommerce)
Unique product configurators or bundles Strong fit Limited without apps
Headless commerce with a custom frontend Strong fit Possible via APIs
Tight integration with internal systems Strong fit Depends on apps and plugins
Fastest launch with standard features Slower to build Faster
Lowest maintenance Requires developers Platform handles most

Many teams combine both: a React frontend with a commerce backend's APIs (headless). Backend choices are explored in custom backend solutions vs. off-the-shelf software.

Cart Architecture Overview

  1. Product data from your API or CMS
  2. Cart state (items, quantities, selected options)
  3. Actions (add, remove, update quantity, clear)
  4. Derived values (item count, subtotal)
  5. Persistence (localStorage for guests, server for logged-in users)
  6. UI components (cart button, drawer, line items, summary)
  7. Checkout (server creates a payment session with validated prices)

Full-stack architecture is covered in building a MERN stack application: step-by-step guide.

Step 1: Define the Cart Reducer

A reducer keeps cart logic predictable and testable:

// cartReducer.js
export const initialCart = { items: [] };

export function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM": {
      const { id, variant = "default", qty = 1 } = action.payload;
      const key = `${id}-${variant}`;
      const existing = state.items.find(i => i.key === key);
      if (existing) {
        return { ...state, items: state.items.map(i =>
          i.key === key ? { ...i, qty: i.qty + qty } : i) };
      }
      return { ...state, items: [...state.items, { ...action.payload, key, qty }] };
    }
    case "UPDATE_QTY":
      return { ...state, items: state.items
        .map(i => i.key === action.key ? { ...i, qty: Math.max(0, action.qty) } : i)
        .filter(i => i.qty > 0) };
    case "REMOVE_ITEM":
      return { ...state, items: state.items.filter(i => i.key !== action.key) };
    case "CLEAR":
      return initialCart;
    default:
      return state;
  }
}

Using a key made from product ID plus variant lets the same product in different sizes or colors appear as separate lines.

Step 2: Create a Cart Context and Provider

// CartContext.jsx
import { createContext, useContext, useEffect, useMemo, useReducer } from "react";
import { cartReducer, initialCart } from "./cartReducer";

const CartContext = createContext(null);

function loadCart() {
  try { return JSON.parse(localStorage.getItem("cart")) || initialCart; }
  catch { return initialCart; }
}

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialCart, loadCart);

  useEffect(() => {
    try { localStorage.setItem("cart", JSON.stringify(state)); } catch {}
  }, [state]);

  const totals = useMemo(() => {
    const count = state.items.reduce((n, i) => n + i.qty, 0);
    const subtotal = state.items.reduce((s, i) => s + i.price * i.qty, 0);
    return { count, subtotal };
  }, [state.items]);

  const value = useMemo(() => ({ ...state, ...totals, dispatch }), [state, totals]);
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

export const useCart = () => useContext(CartContext);

The try/catch blocks protect against browsers where storage is blocked. For larger apps, Zustand or Redux Toolkit can replace Context to reduce unnecessary re-renders.

Step 3: Build Cart Components

// AddToCartButton.jsx
import { useCart } from "./CartContext";
export function AddToCartButton({ product, variant }) {
  const { dispatch } = useCart();
  return (
    <button onClick={() => dispatch({ type: "ADD_ITEM",
      payload: { id: product.id, name: product.name, price: product.price, variant } })}>
      Add to cart
    </button>
  );
}

Other components to build:

  • CartButton: header icon with item count badge
  • CartDrawer: slide-out panel showing items without leaving the page
  • CartLineItem: image, name, variant, quantity stepper, remove button
  • CartSummary: subtotal, estimated shipping, discounts, and checkout button
  • EmptyCart: friendly message and product suggestions

Format prices with Intl.NumberFormat for correct currency display.

[ Insert infographic image here — generation prompt in the comment below ]

Step 4: Sync Carts for Logged-In Users

Guest carts can live in localStorage, but logged-in users expect their cart on every device:

  • Save cart changes to your API (debounced to avoid excessive requests)
  • Merge the guest cart into the user cart at login
  • Reload server cart data on app start
  • Revalidate prices and stock when the cart loads

Step 5: Integrate Secure Checkout

Never send totals from the browser as the source of truth. Instead:

  1. Send item IDs, variants, and quantities to your server
  2. The server looks up current prices, discounts, taxes, and inventory
  3. The server creates a checkout session with a payment provider (such as Stripe Checkout)
  4. Redirect the user to the hosted payment page or use embedded payment elements
  5. Use webhooks to confirm payment and create the order
  6. Clear the cart after successful payment

Using hosted payment fields means card data never touches your servers, simplifying PCI compliance. Security fundamentals are covered in e-commerce security: protecting your customers and business and top security practices in backend development.

Step 6: Optimize Cart UX

UX Feature Why It Helps
Cart drawer instead of page redirect Keeps shoppers browsing
Instant feedback ("Added to cart") Confirms the action
Editable quantities and variants Reduces friction
Shipping threshold progress bar Encourages larger orders
Product recommendations in cart Increases average order value
Clear total with taxes/shipping estimates Prevents checkout surprises
Guest checkout option Reduces abandonment
Saved cart and abandoned cart emails Recovers lost sales

Checkout UX is covered in e-commerce checkout optimization and reduce shopping cart abandonment.

Step 7: Make the Cart Accessible

  • Use real <button> elements with accessible labels ("Increase quantity of Blue T-Shirt")
  • Announce cart updates with an aria-live region
  • Trap focus inside the open cart drawer and close it with Escape
  • Ensure color contrast and visible focus states
  • Support keyboard navigation for quantity controls

Step 8: Performance and Testing

  • Memoize derived values and components to avoid unnecessary re-renders
  • Lazy-load the cart drawer if it's heavy
  • Use optimistic UI updates, with rollback if a server request fails
  • Write unit tests for reducer logic (add, update, remove, merge)
  • Add end-to-end tests for the full add-to-cart and checkout flow

Frontend-backend integration practices are in integrating frontend and backend best practices.

Handling Discounts, Taxes, and Shipping

Carts often need more than a subtotal. Keep these calculations server-driven:

  • Discount codes: let users enter a code, send it to an API endpoint that validates eligibility, expiry, and minimum spend, and return the adjusted totals
  • Taxes: calculate on the server using the shipping address and a tax service, since rates vary by location
  • Shipping: fetch live rates or rule-based options (flat rate, free over a threshold) from your backend once an address is known
  • Display: show "Estimated" labels until the final figures are confirmed at checkout

Storing the server's response in a separate pricing state — rather than mixing it into cart items — keeps the reducer simple and avoids showing stale totals.

Choosing a State Management Approach

Approach Best For Trade-offs
Context + useReducer Small to medium stores, few cart consumers Built in; may cause extra re-renders in large trees
Zustand Medium to large apps wanting minimal boilerplate Tiny library; selective subscriptions reduce re-renders
Redux Toolkit Large teams and complex state with dev tooling More structure and setup
Server state (React Query, SWR) Carts stored primarily on the server Pairs well with any client store for UI state

Start simple and move to a dedicated library only when performance or complexity requires it.

Key Takeaways

  • Centralize cart state with Context and useReducer, or a state library for larger apps.
  • Define clear actions and derive totals from state rather than storing them separately.
  • Persist guest carts in localStorage and sync logged-in carts with the server.
  • Validate prices, discounts, and inventory on the server and use a secure payment provider.
  • Optimize cart UX and accessibility, and test reducer logic and checkout flows.

Frequently Asked Questions

How do I manage shopping cart state in React?

Use a central state solution such as React Context with useReducer for small to medium apps, or libraries like Zustand or Redux Toolkit for larger apps. Define actions for adding, updating, removing, and clearing items, and derive totals from the cart items.

How do I keep a React cart after page refresh?

Save the cart state to localStorage whenever it changes and load it when the app starts, wrapping storage access in error handling. For logged-in users, also sync the cart to your server so it persists across devices.

Is it safe to calculate cart totals in the browser?

You can display totals in the browser for user experience, but the server must recalculate prices, discounts, taxes, and inventory at checkout. Never trust client-side totals, because users can modify browser data.

How do I add payments to a React shopping cart?

Send cart item IDs and quantities to your server, validate them, and create a checkout session with a payment provider such as Stripe. Redirect users to hosted checkout or use secure embedded payment components, then confirm orders with webhooks.

Should I build a custom cart or use Shopify?

Use a hosted platform if you need standard e-commerce features quickly with low maintenance. Build a custom React cart, or a headless setup, when you need unique product configurators, custom UX, or deep integrations with internal systems.

Need a custom e-commerce experience built in React? AiScope Digital's website development and design services build fast, secure, headless and custom shopping experiences — get a free consultation today.