← Back to Tutorials
React

Build an E-commerce Store with React

Difficulty: Advanced Est. Time: ~8 hours

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.

What You'll Build

A complete e-commerce store with:

  • Product catalog with categories
  • Shopping cart functionality
  • User authentication
  • Checkout flow
  • Order history
  • Payment integration (Stripe)
What You'll Learn
  • 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:

  1. User browses products → Server returns product data
  2. User adds to cart → Cart stored in state/localStorage
  3. User proceeds to checkout → Form data collected
  4. Payment processed → Payment gateway validates
  5. Order created → Database updated with order
  6. Confirmation shown → Email notification sent
Why React?

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

File 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
New to React?

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

Bash
# 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

Bash
# 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:

JavaScript
// 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

JavaScript
// 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;
Understanding Routes

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

JavaScript
// 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;
}
Why 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

JavaScript
// 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.

JavaScript
// 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 Explained

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.

JavaScript
// 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>;
}
Calculating Totals

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.

JavaScript
// 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>;
}
Security Note

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.

JavaScript
// 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>;
}
How Stripe Works

With Stripe, sensitive card data never touches your server:

  1. User enters card details in Stripe's secure form
  2. Stripe creates a "payment method" (not a charge)
  3. You send the payment method ID to your server
  4. 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
Bash
# 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

Continue Learning

Ready for more? Try these tutorials:

  • Build a Real-time Chat App - Learn WebSockets
  • Build a Blog CMS - Full-stack development
  • Build a URL Shortener - API design