Stateful Shopping Cart Prototyping: Add to Cart, Quantities, and Checkout Flow
Stateful Shopping Cart Prototyping: Add to Cart, Quantities, and Checkout Flow
Building an e-commerce shopping cart is one of the most demanding frontend tasks. A cart is not a simple read-only data view; it is a highly stateful, event-driven system requiring:
- Optimistic UI Updates: Instant user feedback when clicking "Add to Cart" or incrementing items.
- Race Condition Prevention: Debouncing rapid quantity clicks so out-of-order network responses don't corrupt the cart.
- Dynamic Price Reconciliation: Calculating line-item totals, discount codes, tiered shipping thresholds, and tax estimates.
- State Persistence: Preserving items when a user navigates between product catalog pages and checkout screens.
In this guide, we review the architecture of modern shopping carts, implement an optimistic cart manager in React, and connect it with Gen-Endpoint's free Cart API and Products API.
Why Stateless Mocks Break Down in E-Commerce
Most free mock APIs are completely stateless. When you send a POST /cart request to add an item, a stateless mock returns { "success": true }, but when your checkout page later requests GET /cart, it returns an empty array or a hardcoded default list.
This forces frontend developers to maintain complex in-memory mock hacks in their local components, which hides bugs like:
- Inventory caps (e.g., maximum 5 items per customer).
- Calculation discrepancies between client math and server roundoffs.
- Coupon code validations and discount expirations.
Using Gen-Endpoint's stateful Cart API, your items persist throughout your session, allowing true end-to-end testing from catalog to checkout.
The E-Commerce Cart Data Contract
Before building the UI, define the data contract:
// types/cart.ts
export interface Product {
id: string;
title: string;
price: number;
image: string;
stock: number;
}
export interface CartItem {
id: string;
productId: string;
title: string;
unitPrice: number;
quantity: number;
itemTotal: number;
image: string;
}
export interface CartSummary {
items: CartItem[];
subtotal: number;
discount: number;
shipping: number;
tax: number;
grandTotal: number;
}
Building an Optimistic Shopping Cart in React
When an online shopper clicks + to increase an item quantity, waiting 600ms for a network round-trip makes the interface feel sluggish and unresponsive.
Optimistic updates solve this: We update the React state immediately so the counter advances instantly, send the mutation to the backend in the background, and roll back to the previous state only if the request fails:
// hooks/useCart.ts
'use client';
import { useState, useEffect, useCallback } from 'react';
import type { CartItem, CartSummary } from '@/types/cart';
export function useCart() {
const [cart, setCart] = useState<CartSummary>({
items: [],
subtotal: 0,
discount: 0,
shipping: 0,
tax: 0,
grandTotal: 0,
});
const [isLoading, setIsLoading] = useState<boolean>(true);
// Recalculate financial totals
const recalculateSummary = (items: CartItem[]): CartSummary => {
const subtotal = items.reduce((acc, item) => acc + item.unitPrice * item.quantity, 0);
const shipping = subtotal > 50 || subtotal === 0 ? 0 : 5.99; // Free shipping over $50
const tax = Number((subtotal * 0.08).toFixed(2)); // 8% sales tax estimate
const discount = 0;
const grandTotal = Number((subtotal + shipping + tax - discount).toFixed(2));
return { items, subtotal, discount, shipping, tax, grandTotal };
};
// Fetch initial cart state from Gen-Endpoint
useEffect(() => {
async function loadCart() {
try {
const res = await fetch('https://gen-endpoint.com/api/cart');
if (res.ok) {
const data = await res.json();
const items = Array.isArray(data.items) ? data.items : [];
setCart(recalculateSummary(items));
}
} catch (err) {
console.error('Failed to load initial cart:', err);
} finally {
setIsLoading(false);
}
}
loadCart();
}, []);
// Optimistic quantity update
const updateQuantity = useCallback(async (itemId: string, newQuantity: number) => {
if (newQuantity < 1) return;
// 1. Snapshot previous state for rollback
const previousCart = cart;
// 2. Compute optimistic items
const updatedItems = cart.items.map((item) =>
item.id === itemId
? { ...item, quantity: newQuantity, itemTotal: item.unitPrice * newQuantity }
: item
);
// 3. Immediately update UI
setCart(recalculateSummary(updatedItems));
// 4. Send background sync to API
try {
const response = await fetch(`https://gen-endpoint.com/api/cart/${itemId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantity: newQuantity }),
});
if (!response.ok) {
throw new Error('Server rejected quantity update');
}
} catch (err) {
console.error('Mutation failed, rolling back cart:', err);
// Rollback on failure
setCart(previousCart);
alert('Could not update quantity. Please check your internet connection.');
}
}, [cart]);
// Remove item from cart
const removeItem = useCallback(async (itemId: string) => {
const previousCart = cart;
const updatedItems = cart.items.filter((item) => item.id !== itemId);
setCart(recalculateSummary(updatedItems));
try {
const res = await fetch(`https://gen-endpoint.com/api/cart/${itemId}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error('Deletion failed');
} catch (err) {
setCart(previousCart);
}
}, [cart]);
return { cart, isLoading, updateQuantity, removeItem };
}
The Cart Checkout Drawer Component
Here is a full cart drawer component with responsive layout and total breakdowns:
// components/CartDrawer.tsx
'use client';
import React from 'react';
import { useCart } from '@/hooks/useCart';
export function CartDrawer() {
const { cart, isLoading, updateQuantity, removeItem } = useCart();
if (isLoading) {
return <div className="p-8 text-center text-sm">Loading your cart...</div>;
}
if (cart.items.length === 0) {
return (
<div className="p-12 text-center space-y-3">
<h3 className="text-lg font-bold">Your Cart is Empty</h3>
<p className="text-sm text-muted-foreground">Add products to begin checkout.</p>
</div>
);
}
return (
<div className="max-w-md mx-auto p-6 bg-card border rounded-2xl shadow-lg space-y-6">
<h2 className="text-xl font-bold tracking-tight">Your Order ({cart.items.length} items)</h2>
{/* Item List */}
<div className="divide-y divide-border">
{cart.items.map((item) => (
<div key={item.id} className="py-4 flex items-center justify-between gap-4">
<div>
<h4 className="font-semibold text-sm">{item.title}</h4>
<p className="text-xs text-muted-foreground">${item.unitPrice.toFixed(2)} each</p>
</div>
{/* Quantity Controls */}
<div className="flex items-center gap-2">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="w-7 h-7 rounded border flex items-center justify-center font-bold text-sm hover:bg-muted"
>
-
</button>
<span className="w-6 text-center text-sm font-semibold">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-7 h-7 rounded border flex items-center justify-center font-bold text-sm hover:bg-muted"
>
+
</button>
</div>
<div className="text-right">
<p className="font-bold text-sm">${item.itemTotal.toFixed(2)}</p>
<button
onClick={() => removeItem(item.id)}
className="text-xs text-destructive hover:underline"
>
Remove
</button>
</div>
</div>
))}
</div>
{/* Financial Summary */}
<div className="p-4 bg-muted/40 rounded-xl space-y-2 text-sm">
<div className="flex justify-between text-muted-foreground">
<span>Subtotal</span>
<span>${cart.subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between text-muted-foreground">
<span>Shipping</span>
<span>{cart.shipping === 0 ? 'FREE' : `$${cart.shipping.toFixed(2)}`}</span>
</div>
<div className="flex justify-between text-muted-foreground">
<span>Estimated Tax</span>
<span>${cart.tax.toFixed(2)}</span>
</div>
<div className="border-t pt-2 flex justify-between font-bold text-base text-foreground">
<span>Total</span>
<span>${cart.grandTotal.toFixed(2)}</span>
</div>
</div>
<button
onClick={() => alert('Proceeding to checkout with total: $' + cart.grandTotal)}
className="w-full py-3 rounded-xl bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors shadow-sm"
>
Proceed to Checkout
</button>
</div>
);
}
Summary & Next Steps
Prototyping e-commerce flows requires realistic data persistence and optimistic mutation updates. With Gen-Endpoint's Cart API and Products API, you can stress-test edge cases, shipping calculations, and checkout transitions without writing a single line of backend database code.
- Explore the Cart REST API Documentation.
- Explore the Products REST API Documentation.
Ready to Test These Endpoints?
Access 23+ free mock REST APIs, test file uploads, and generate Next.js API handlers with zero signup.