Build an E-commerce Store with React
Introduction
E-commerce is one of the most popular use cases for web development. Whether you're building a small online store or a large marketplace, the fundamental concepts remain the same.
In this comprehensive tutorial, you'll build a fully-featured e-commerce store using React. You'll learn modern frontend development patterns, state management, and how to integrate with payment processors.
A complete e-commerce store with:
- Product catalog with categories
- Shopping cart functionality
- User authentication
- Checkout flow
- Order history
- Payment integration (Stripe)
- React fundamentals and hooks
- State management with Context API
- Routing with React Router
- API integration
- Payment processing with Stripe
- Responsive design patterns
E-commerce Basics
Before we start coding, let's understand the key components of an e-commerce application.
Core Components
- Product Catalog - Display products with images, descriptions, and prices
- Shopping Cart - Hold items user wants to purchase
- Checkout - Collect shipping and payment info
- User Accounts - Save orders and preferences
- Admin Dashboard - Manage products and orders
How E-commerce Data Flows
Here's the typical flow when a user makes a purchase:
- User browses products → Server returns product data
- User adds to cart → Cart stored in state/localStorage
- User proceeds to checkout → Form data collected
- Payment processed → Payment gateway validates
- Order created → Database updated with order
- Confirmation shown → Email notification sent
React is perfect for e-commerce because:
- Component-based - Reusable product cards, cart items, etc.
- Fast updates - No page reloads when adding to cart
- Great ecosystem - Many e-commerce libraries available
- Large community - Easy to find solutions to problems
Project Overview
Our e-commerce store will have these features:
Customer-Facing Features
- Home page with featured products
- Product listing with filtering and sorting
- Product detail pages
- Shopping cart (persistent)
- Checkout process
- Order confirmation
Technical Stack
- Frontend: React with Vite
- Routing: React Router v6
- State: React Context API
- Styling: CSS Modules or styled-components
- Payments: Stripe
- Backend (mock): JSON Server or simple API
Project Structure
ecommerce-store/
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ ├── ProductCard.jsx
│ │ ├── CartItem.jsx
│ │ └── Button.jsx
│ ├── pages/ # Page components
│ │ ├── Home.jsx
│ │ ├── Products.jsx
│ │ ├── ProductDetail.jsx
│ │ ├── Cart.jsx
│ │ ├── Checkout.jsx
│ │ └── Orders.jsx
│ ├── context/ # React Context
│ │ ├── CartContext.jsx
│ │ └── AuthContext.jsx
│ ├── hooks/ # Custom hooks
│ ├── data/ # Mock data
│ ├── App.jsx # Main app component
│ └── main.jsx # Entry point
Prerequisites
Before starting this tutorial, ensure you have:
- Node.js installed - Version 14 or higher
- Code editor - VS Code recommended
- JavaScript proficiency - ES6+ features
- Basic React knowledge - Components and props
If you're new to React, I recommend completing the "Build a Task Manager" tutorial first, which covers React basics. Then come back to this advanced tutorial!
Project Setup
Let's set up our React project with Vite for fast development.
Creating the Project
# Create React project with Vite
npm create vite@latest ecommerce-store -- --template react
cd ecommerce-store
# Install dependencies
npm install
npm install react-router-dom @stripe/stripe-js @stripe/react-stripe-js
# What each package does:
# react-router-dom - Navigation between pages
# @stripe/stripe-js - Stripe payment integration
# @stripe/react-stripe-js - React components for Stripe
Setting Up Project Structure
# Create folder structure
cd src
mkdir -p components pages context hooks data
# Verify structure
find . -type d | head -20
Setting Up React
Now let's set up the main application structure with routing.
Creating Mock Data
First, let's create some sample product data:
// src/data/products.js
export const products = [
{
id: 1,
name: "Classic White T-Shirt",
description: "A comfortable cotton t-shirt perfect for everyday wear.",
price: 29.99,
image: "/images/product-1.jpg",
category: "clothing",
stock: 50
},
{
id: 2,
name: "Denim Jeans",
description: "Classic fit denim jeans with a modern style.",
price: 79.99,
image: "/images/product-2.jpg",
category: "clothing",
stock: 30
},
{
id: 3,
name: "Running Shoes",
description: "Lightweight running shoes with superior cushioning.",
price: 129.99,
image: "/images/product-3.jpg",
category: "footwear",
stock: 25
},
{
id: 4,
name: "Leather Wallet",
description: "Genuine leather wallet with multiple card slots.",
price: 49.99,
image: "/images/product-4.jpg",
category: "accessories",
stock: 40
}
];
export const categories = ["clothing", "footwear", "accessories"];
Main App with Routing
// src/App.jsx
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import Cart from './pages/Cart';
import Checkout from './pages/Checkout';
import './App.css';
function App() {
return <?><Router>
<div className="app">
<Header />
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/cart" element={<Cart />} />
<Route path="/checkout" element={<Checkout />} />
</Routes>
</main>
<Footer />
</div>
</Router>;
}
export default App;
Routes define which component displays based on the URL:
/→ Home page/products→ Product listing/products/:id→ Individual product (dynamic)/cart→ Shopping cart/checkout→ Checkout page
The :id is a URL parameter - it lets us access the product ID in the ProductDetail component!
State Management with Context
We need to manage cart state across the entire application. React Context is perfect for this.
Creating the Cart Context
// src/context/CartContext.jsx
import { createContext, useState, useContext, useEffect } from 'react';
// Create the context
const CartContext = createContext();
// Cart provider component
export function CartProvider({ children }) {
// Initialize cart from localStorage
const [cart, setCart] = useState(() => {
const saved = localStorage.getItem('cart');
return saved ? JSON.parse(saved) : [];
});
// Save to localStorage whenever cart changes
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
// Add item to cart
const addToCart = (product, quantity = 1) => {
setCart(prevCart => {
// Check if item already exists
const existingItem = prevCart.find(item => item.id === product.id);
if (existingItem) {
// Update quantity
return prevCart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
);
} else {
// Add new item
return [...prevCart, { ...product, quantity }];
}
});
};
// Remove item from cart
const removeFromCart = (productId) => {
setCart(prevCart => prevCart.filter(item => item.id !== productId));
};
// Update quantity
const updateQuantity = (productId, quantity) => {
if (quantity < 1) return removeFromCart(productId);
setCart(prevCart => prevCart.map(item =>
item.id === productId ? { ...item, quantity } : item
));
};
// Clear entire cart
const clearCart = () => setCart([]);
// Calculate totals
const cartTotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
return <?><CartContext.Provider value={{
cart,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
cartTotal,
cartCount
}}>
{children}
</CartContext.Provider>;
};
// Custom hook for easy access to cart
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
Context lets us share state across components without passing props through every level (called "prop drilling"). Now any component can access the cart with a simple useCart() hook!
Wrapping the App with Provider
// src/main.jsx (update)
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { CartProvider } from './context/CartContext.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<CartProvider>
<App />
</CartProvider>
</React.StrictMode>,
)
Product Listing Page
Now let's create the products page that displays our inventory.
// src/pages/Products.jsx
import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { products, categories } from '../data/products';
import ProductCard from '../components/ProductCard';
export default function Products() {
const [selectedCategory, setSelectedCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
const [searchQuery, setSearchQuery] = useState('');
// Filter and sort products
const filteredProducts = useMemo(() => {
let result = [...products];
// Filter by category
if (selectedCategory !== 'all') {
result = result.filter(p => p.category === selectedCategory);
}
// Filter by search query
if (searchQuery) {
result = result.filter(p =>
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Sort results
result.sort((a, b) => {
switch(sortBy) {
case 'price-low':
return a.price - b.price;
case 'price-high':
return b.price - a.price;
case 'name':
default:
return a.name.localeCompare(b.name);
}
});
return result;
}, [selectedCategory, sortBy, searchQuery]);
return <?><div className="products-page">
<h1>Our Products</h1>
<!-- Filters -->
<div className="filters">
<input
type="text"
placeholder="Search products..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
<select value={selectedCategory} onChange={e => setSelectedCategory(e.target.value)}>
<option value="all">All Categories</option>
{categories.map(cat => <option key={cat} value={cat}>{cat}</option})}
</select>
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">Name (A-Z)</option>
<option value="price-low">Price: Low to High</option>
<option value="price-high">Price: High to Low</option>
</select>
</div>
<!-- Product Grid -->
<div className="product-grid">
{filteredProducts.map(product => <ProductCard key={product.id} product={product} />)}
</div>
{filteredProducts.length === 0 && (
<p>No products found.</p> )}
</div>;
}
useMemo caches the result of a calculation so it doesn't recalculate on every render. We use it here to filter and sort products efficiently.
Shopping Cart
Let's create the shopping cart page that shows selected items.
// src/pages/Cart.jsx
import { Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';
export default function Cart() {
const { cart, removeFromCart, updateQuantity, cartTotal, clearCart } = useCart();
if (cart.length === 0) {
return <?><div className="cart-empty">
<h2>Your cart is empty</h2>
<Link to="/products" className="btn">Continue Shopping</Link>
</div>;
}
return <?><div className="cart-page">
<h1>Shopping Cart</h1>
<div className="cart-items">
{cart.map(item => <?><div key={item.id} className="cart-item">
<img src={item.image} alt={item.name} />
<div className="item-details">
<h3>{item.name}</h3>
<p>${item.price.toFixed(2)}</p>
</div>
<div className="quantity-controls">
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
</div>
<p>${(item.price * item.quantity).toFixed(2)}</p>
<button onClick={() removeFromCart(item.id)} className="remove-btn">✕</button>
</div>)}
</div>
<!-- Cart Summary -->
<div className="cart-summary">
<h3>Order Summary</h3>
<div>
<span>Subtotal:</span>
<span>${cartTotal.toFixed(2)}</span>
</div>
<div>
<span>Shipping:</span>
<span>${cartTotal > 100 ? 'Free' : '10.00'}</span>
</div>
<div>
<span>Tax (8%):</span>
<span>${(cartTotal * 0.08).toFixed(2)}</span>
</div>
<hr />
<div>
<strong>Total:</strong>
<strong>${(cartTotal * 1.08 + (cartTotal > 100 ? 0 : 10)).toFixed(2)}</strong>
</div>
<Link to="/checkout" className="btn btn-checkout">
Proceed to Checkout
</Link>
</div>
</div>;
}
Notice how we calculate the order summary:
- Subtotal: Sum of (price × quantity) for all items
- Shipping: Free for orders over $100
- Tax: 8% of subtotal
- Total: Subtotal + Shipping + Tax
Checkout Flow
The checkout process collects shipping and payment information.
// src/pages/Checkout.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCart } from '../context/CartContext';
export default function Checkout() {
const { cart, cartTotal, clearCart } = useCart();
const navigate = useNavigate();
const [isProcessing, setIsProcessing] = useState(false);
const [formData, setFormData] = useState({
email: '',
name: '',
address: '',
city: '',
zip: '',
cardNumber: '',
expiry: '',
cvc: ''
});
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsProcessing(true);
// Simulate payment processing (replace with actual Stripe)
await new Promise(resolve => setTimeout(resolve, 2000));
// Clear cart and show confirmation
clearCart();
alert('Order placed successfully!');
navigate('/');
};
return <?><div className="checkout-page">
<h1>Checkout</h1>
<form onSubmit={handleSubmit} className="checkout-form">
<!-- Shipping Information -->
<section>
<h2>Shipping Information</h2>
<input
type="email"
name="email"
placeholder="Email"
value={formData.email}
onChange={handleChange}
required
/>
<input
type="text"
name="name"
placeholder="Full Name"
value={formData.name}
onChange={handleChange}
required
/>
<input
type="text"
name="address"
placeholder="Address"
value={formData.address}
onChange={handleChange}
required
/>
<div>
<input
type="text"
name="city"
placeholder="City"
value={formData.city}
onChange={handleChange}
required
/>
<input
type="text"
name="zip"
placeholder="ZIP Code"
value={formData.zip}
onChange={handleChange}
required
/>
</div>
</section>
<!-- Payment Information -->
<section>
<h2>Payment Information</h2>
<input
type="text"
name="cardNumber"
placeholder="Card Number"
value={formData.cardNumber}
onChange={handleChange}
required
/>
<div>
<input
type="text"
name="expiry"
placeholder="MM/YY"
value={formData.expiry}
onChange={handleChange}
required
/>
<input
type="text"
name="cvc"
placeholder="CVC"
value={formData.cvc}
onChange={handleChange}
required
/>
</div>
</section>
<!-- Order Total -->
<div className="order-total">
<span>Total:</span>
<span>${cartTotal.toFixed(2)}</span>
</div>
<button type="submit" disabled={isProcessing} className="btn btn-place-order">
{isProcessing ? 'Processing...' : 'Place Order'}
</button>
</form>
</div>;
}
This is a demo checkout - never process real payments this way! In production, you'd use Stripe Elements or a similar payment form that handles sensitive card data securely. Never store card numbers in your database!
Payment Integration (Stripe)
Let's see how to integrate real payments with Stripe.
// Setting up Stripe (conceptual)
// 1. Get Stripe publishable key from Stripe Dashboard
const stripePromise = loadStripe('pk_test_your_key_here');
// 2. Wrap your checkout with Elements provider
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
function CheckoutPage() {
return <?><Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>;
}
// 3. Use Stripe's CardElement in your form
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (event) => {
event.preventDefault();
// Create payment method
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardElement),
});
if (error) {
console.log('[error]', error);
} else {
console.log('[PaymentMethod]', paymentMethod);
// Send paymentMethod.id to your server
}
};
return <form onSubmit={handleSubmit}>
<CardElement />
<button type="submit" disabled={!stripe}>Pay</button>
</form>;
}
With Stripe, sensitive card data never touches your server:
- User enters card details in Stripe's secure form
- Stripe creates a "payment method" (not a charge)
- You send the payment method ID to your server
- Your server creates the actual charge
Deployment
When you're ready to deploy your e-commerce store, here are your options.
Frontend Deployment
- Vercel - Best for React, free tier excellent
- Netlify - Great alternative, easy setup
- Cloudflare Pages - Free, fast CDN
# Deploy to Vercel
npm install -g vercel
vercel
# Or use the Vercel button on GitHub
Important Production Considerations
- Use environment variables for API keys
- Set up a real backend for order processing
- Implement proper authentication
- Add HTTPS (automatic on Vercel/Netlify)
- Set up error tracking (Sentry)
Summary
Congratulations! You've built a complete e-commerce store from scratch.
What You Built
- React Application - Modern component-based architecture
- Product Catalog - Filterable product listing
- Shopping Cart - Persistent cart with Context API
- Checkout Flow - Multi-step checkout process
- Payment Integration - Stripe integration ready
Key Concepts Learned
- React Router for navigation
- Context API for global state
- Component composition
- Form handling
- Payment processing concepts
Next Steps
Add these features to make it production-ready:
- User authentication (login/signup)
- Order history and tracking
- Product reviews and ratings
- Wishlist functionality
- Admin dashboard
- Email notifications