Zum Inhalt springen
React Anfänger 75 min

Projekt: Online-Shop

Baue einen vollständigen Online-Shop mit React. Produktliste, Warenkorb, Filter und Checkout - alles mit modernem State Management.

Aktualisiert:

Projekt: Online-Shop

In diesem Projekt baust du einen vollstaendigen Online-Shop mit React. Du lernst, wie du Produktlisten, einen Warenkorb, Filter und einen Checkout-Prozess implementierst – mit sauberem State Management.

Was wir bauen

  • Produktliste mit Bildern, Preisen und Kategorien
  • Warenkorb mit Hinzufuegen, Entfernen und Mengensteuerung
  • Produktfilter nach Kategorie und Preis
  • Produktsuche mit Live-Ergebnissen
  • Checkout-Seite mit Bestelluebersicht
  • Responsive Layout mit React Router

Projekt aufsetzen

npm create vite@latest react-shop -- --template react
cd react-shop
npm install react-router-dom
npm run dev

Projektstruktur

src/
├── components/
│   ├── Layout.jsx
│   ├── ProductCard.jsx
│   ├── CartItem.jsx
│   ├── SearchBar.jsx
│   └── CategoryFilter.jsx
├── context/
│   └── CartContext.jsx
├── data/
│   └── products.js
├── pages/
│   ├── Home.jsx
│   ├── ProductDetail.jsx
│   ├── Cart.jsx
│   └── Checkout.jsx
├── App.jsx
└── main.jsx

Schritt 1: Produktdaten

// src/data/products.js
export const products = [
  {
    id: 1,
    name: 'Wireless Kopfhoerer',
    price: 79.99,
    category: 'elektronik',
    image: 'https://picsum.photos/seed/headphones/400/400',
    description: 'Kabellose Bluetooth-Kopfhoerer mit aktiver Geraeuschdaempfung und 30 Stunden Akkulaufzeit.',
    rating: 4.5,
    inStock: true
  },
  {
    id: 2,
    name: 'Mechanische Tastatur',
    price: 129.99,
    category: 'elektronik',
    image: 'https://picsum.photos/seed/keyboard/400/400',
    description: 'RGB mechanische Gaming-Tastatur mit Cherry MX Switches und Handballenauflage.',
    rating: 4.8,
    inStock: true
  },
  {
    id: 3,
    name: 'React-Handbuch',
    price: 34.99,
    category: 'buecher',
    image: 'https://picsum.photos/seed/reactbook/400/400',
    description: 'Umfassendes Handbuch fuer React 19 mit praktischen Projekten und Best Practices.',
    rating: 4.7,
    inStock: true
  },
  {
    id: 4,
    name: 'Laptop-Rucksack',
    price: 49.99,
    category: 'zubehoer',
    image: 'https://picsum.photos/seed/backpack/400/400',
    description: 'Wasserabweisender Rucksack mit gepolstertem Laptopfach fuer bis zu 15.6 Zoll.',
    rating: 4.3,
    inStock: true
  },
  {
    id: 5,
    name: 'USB-C Hub',
    price: 39.99,
    category: 'elektronik',
    image: 'https://picsum.photos/seed/usbhub/400/400',
    description: '7-in-1 USB-C Hub mit HDMI, USB 3.0, SD-Kartenleser und Ethernet.',
    rating: 4.4,
    inStock: false
  },
  {
    id: 6,
    name: 'JavaScript Guide',
    price: 29.99,
    category: 'buecher',
    image: 'https://picsum.photos/seed/jsbook/400/400',
    description: 'Von Grundlagen bis zu fortgeschrittenen Konzepten – alles ueber modernes JavaScript.',
    rating: 4.6,
    inStock: true
  },
  {
    id: 7,
    name: 'Webcam HD',
    price: 59.99,
    category: 'elektronik',
    image: 'https://picsum.photos/seed/webcam/400/400',
    description: '1080p Webcam mit Mikrofon und Autofokus. Perfekt fuer Videocalls.',
    rating: 4.2,
    inStock: true
  },
  {
    id: 8,
    name: 'Mousepad XL',
    price: 19.99,
    category: 'zubehoer',
    image: 'https://picsum.photos/seed/mousepad/400/400',
    description: 'Grosses Gaming-Mousepad mit rutschfester Unterseite. 80x30cm.',
    rating: 4.5,
    inStock: true
  }
];

Schritt 2: Cart-Context mit useReducer

// src/context/CartContext.jsx
import { createContext, useContext, useReducer } from 'react';

const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_TO_CART': {
      const existing = state.find(item => item.id === action.payload.id);
      if (existing) {
        return state.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...state, { ...action.payload, quantity: 1 }];
    }

    case 'REMOVE_FROM_CART':
      return state.filter(item => item.id !== action.payload);

    case 'UPDATE_QUANTITY':
      if (action.payload.quantity <= 0) {
        return state.filter(item => item.id !== action.payload.id);
      }
      return state.map(item =>
        item.id === action.payload.id
          ? { ...item, quantity: action.payload.quantity }
          : item
      );

    case 'CLEAR_CART':
      return [];

    default:
      return state;
  }
}

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

  const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);

  function addToCart(product) {
    dispatch({ type: 'ADD_TO_CART', payload: product });
  }

  function removeFromCart(id) {
    dispatch({ type: 'REMOVE_FROM_CART', payload: id });
  }

  function updateQuantity(id, quantity) {
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
  }

  function clearCart() {
    dispatch({ type: 'CLEAR_CART' });
  }

  return (
    <CartContext.Provider value={{
      cart, totalItems, totalPrice,
      addToCart, removeFromCart, updateQuantity, clearCart
    }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) throw new Error('useCart muss in CartProvider verwendet werden');
  return context;
}

Schritt 3: Layout mit Navigation

// src/components/Layout.jsx
import { Outlet, NavLink, Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';

function Layout() {
  const { totalItems } = useCart();

  const linkStyle = ({ isActive }) => ({
    textDecoration: 'none',
    color: isActive ? '#2563eb' : '#475569',
    fontWeight: isActive ? 600 : 400
  });

  return (
    <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
      <header style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        padding: '1rem 2rem',
        borderBottom: '1px solid #e2e8f0',
        backgroundColor: 'white',
        position: 'sticky',
        top: 0,
        zIndex: 100
      }}>
        <Link to="/" style={{
          fontSize: '1.5rem',
          fontWeight: 800,
          textDecoration: 'none',
          color: '#2563eb'
        }}>
          ReactShop
        </Link>
        <nav style={{ display: 'flex', gap: '1.5rem', alignItems: 'center' }}>
          <NavLink to="/" end style={linkStyle}>Produkte</NavLink>
          <NavLink to="/cart" style={linkStyle}>
            Warenkorb
            {totalItems > 0 && (
              <span style={{
                marginLeft: '0.25rem',
                padding: '2px 8px',
                backgroundColor: '#2563eb',
                color: 'white',
                borderRadius: '12px',
                fontSize: '0.75rem'
              }}>
                {totalItems}
              </span>
            )}
          </NavLink>
        </nav>
      </header>

      <main style={{ flex: 1, padding: '2rem', maxWidth: '1200px', margin: '0 auto', width: '100%' }}>
        <Outlet />
      </main>

      <footer style={{
        padding: '1.5rem',
        textAlign: 'center',
        borderTop: '1px solid #e2e8f0',
        color: '#94a3b8'
      }}>
        2026 ReactShop – Ein React-Lernprojekt
      </footer>
    </div>
  );
}

export default Layout;

Schritt 4: Produktkarte

// src/components/ProductCard.jsx
import { Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';

function ProductCard({ product }) {
  const { addToCart } = useCart();

  return (
    <div style={{
      border: '1px solid #e2e8f0',
      borderRadius: '12px',
      overflow: 'hidden',
      transition: 'transform 0.2s, box-shadow 0.2s',
      backgroundColor: 'white'
    }}
    onMouseEnter={(e) => {
      e.currentTarget.style.transform = 'translateY(-2px)';
      e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.08)';
    }}
    onMouseLeave={(e) => {
      e.currentTarget.style.transform = '';
      e.currentTarget.style.boxShadow = '';
    }}>
      <Link to={`/product/${product.id}`}>
        <img src={product.image} alt={product.name}
          style={{ width: '100%', height: '200px', objectFit: 'cover' }} />
      </Link>
      <div style={{ padding: '1rem' }}>
        <span style={{
          fontSize: '0.75rem',
          textTransform: 'uppercase',
          color: '#64748b',
          letterSpacing: '0.05em'
        }}>
          {product.category}
        </span>
        <Link to={`/product/${product.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
          <h3 style={{ margin: '0.25rem 0 0.5rem', fontSize: '1.1rem' }}>
            {product.name}
          </h3>
        </Link>
        <div style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center'
        }}>
          <span style={{ fontSize: '1.25rem', fontWeight: 700, color: '#2563eb' }}>
            {product.price.toFixed(2)} €
          </span>
          <button
            onClick={() => addToCart(product)}
            disabled={!product.inStock}
            style={{
              padding: '8px 16px',
              backgroundColor: product.inStock ? '#2563eb' : '#94a3b8',
              color: 'white',
              border: 'none',
              borderRadius: '6px',
              cursor: product.inStock ? 'pointer' : 'not-allowed',
              fontWeight: 600,
              fontSize: '0.85rem'
            }}
          >
            {product.inStock ? 'In den Warenkorb' : 'Ausverkauft'}
          </button>
        </div>
      </div>
    </div>
  );
}

export default ProductCard;

Schritt 5: Startseite mit Filter und Suche

// src/pages/Home.jsx
import { useState, useMemo } from 'react';
import { products } from '../data/products';
import ProductCard from '../components/ProductCard';

function Home() {
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('all');
  const [sortBy, setSortBy] = useState('name');

  const categories = ['all', ...new Set(products.map(p => p.category))];

  const filteredProducts = useMemo(() => {
    return products
      .filter(p => {
        const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
        const matchesCategory = category === 'all' || p.category === category;
        return matchesSearch && matchesCategory;
      })
      .sort((a, b) => {
        if (sortBy === 'price-asc') return a.price - b.price;
        if (sortBy === 'price-desc') return b.price - a.price;
        if (sortBy === 'rating') return b.rating - a.rating;
        return a.name.localeCompare(b.name);
      });
  }, [search, category, sortBy]);

  return (
    <div>
      <h1 style={{ marginBottom: '0.5rem' }}>Unsere Produkte</h1>
      <p style={{ color: '#64748b', marginBottom: '1.5rem' }}>
        {filteredProducts.length} Produkt{filteredProducts.length !== 1 ? 'e' : ''} gefunden
      </p>

      {/* Filter-Leiste */}
      <div style={{
        display: 'flex',
        gap: '1rem',
        marginBottom: '2rem',
        flexWrap: 'wrap',
        alignItems: 'center'
      }}>
        <input
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Produkte suchen..."
          style={{
            padding: '8px 12px',
            border: '1px solid #e2e8f0',
            borderRadius: '6px',
            flex: '1',
            minWidth: '200px'
          }}
        />

        <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
          {categories.map(cat => (
            <button
              key={cat}
              onClick={() => setCategory(cat)}
              style={{
                padding: '6px 14px',
                border: category === cat ? 'none' : '1px solid #e2e8f0',
                borderRadius: '20px',
                backgroundColor: category === cat ? '#2563eb' : 'white',
                color: category === cat ? 'white' : '#475569',
                cursor: 'pointer',
                textTransform: 'capitalize',
                fontSize: '0.85rem'
              }}
            >
              {cat === 'all' ? 'Alle' : cat}
            </button>
          ))}
        </div>

        <select
          value={sortBy}
          onChange={(e) => setSortBy(e.target.value)}
          style={{
            padding: '8px 12px',
            border: '1px solid #e2e8f0',
            borderRadius: '6px'
          }}
        >
          <option value="name">Name A-Z</option>
          <option value="price-asc">Preis aufsteigend</option>
          <option value="price-desc">Preis absteigend</option>
          <option value="rating">Beste Bewertung</option>
        </select>
      </div>

      {/* Produkt-Grid */}
      {filteredProducts.length > 0 ? (
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
          gap: '1.5rem'
        }}>
          {filteredProducts.map(product => (
            <ProductCard key={product.id} product={product} />
          ))}
        </div>
      ) : (
        <div style={{ textAlign: 'center', padding: '3rem', color: '#64748b' }}>
          <p>Keine Produkte gefunden.</p>
          <button onClick={() => { setSearch(''); setCategory('all'); }}
            style={{ marginTop: '1rem', padding: '8px 16px', cursor: 'pointer' }}>
            Filter zuruecksetzen
          </button>
        </div>
      )}
    </div>
  );
}

export default Home;

Schritt 6: Warenkorb-Seite

// src/pages/Cart.jsx
import { Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';

function Cart() {
  const { cart, totalPrice, updateQuantity, removeFromCart, clearCart } = useCart();

  if (cart.length === 0) {
    return (
      <div style={{ textAlign: 'center', padding: '4rem 0' }}>
        <h2>Dein Warenkorb ist leer</h2>
        <p style={{ color: '#64748b', marginBottom: '1.5rem' }}>
          Fuege Produkte hinzu, um loszulegen.
        </p>
        <Link to="/" style={{
          padding: '10px 24px',
          backgroundColor: '#2563eb',
          color: 'white',
          textDecoration: 'none',
          borderRadius: '6px'
        }}>
          Zu den Produkten
        </Link>
      </div>
    );
  }

  return (
    <div>
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '2rem'
      }}>
        <h1>Warenkorb</h1>
        <button onClick={clearCart} style={{
          padding: '6px 14px',
          border: '1px solid #fca5a5',
          color: '#ef4444',
          backgroundColor: 'white',
          borderRadius: '6px',
          cursor: 'pointer'
        }}>
          Warenkorb leeren
        </button>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: '2rem' }}>
        {/* Warenkorb-Artikel */}
        <div>
          {cart.map(item => (
            <div key={item.id} style={{
              display: 'flex',
              gap: '1rem',
              padding: '1rem',
              borderBottom: '1px solid #e2e8f0',
              alignItems: 'center'
            }}>
              <img src={item.image} alt={item.name}
                style={{ width: '80px', height: '80px', objectFit: 'cover', borderRadius: '8px' }} />
              <div style={{ flex: 1 }}>
                <h3 style={{ fontSize: '1rem', marginBottom: '0.25rem' }}>{item.name}</h3>
                <p style={{ color: '#2563eb', fontWeight: 600 }}>{item.price.toFixed(2)} €</p>
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
                <button onClick={() => updateQuantity(item.id, item.quantity - 1)}
                  style={{ width: '32px', height: '32px', border: '1px solid #e2e8f0',
                    borderRadius: '6px', cursor: 'pointer', backgroundColor: 'white' }}>
                  -
                </button>
                <span style={{ width: '30px', textAlign: 'center', fontWeight: 600 }}>
                  {item.quantity}
                </span>
                <button onClick={() => updateQuantity(item.id, item.quantity + 1)}
                  style={{ width: '32px', height: '32px', border: '1px solid #e2e8f0',
                    borderRadius: '6px', cursor: 'pointer', backgroundColor: 'white' }}>
                  +
                </button>
              </div>
              <p style={{ fontWeight: 700, width: '80px', textAlign: 'right' }}>
                {(item.price * item.quantity).toFixed(2)} €
              </p>
              <button onClick={() => removeFromCart(item.id)}
                style={{ color: '#ef4444', background: 'none', border: 'none', cursor: 'pointer' }}>
                Entfernen
              </button>
            </div>
          ))}
        </div>

        {/* Zusammenfassung */}
        <div style={{
          padding: '1.5rem',
          backgroundColor: '#f8fafc',
          borderRadius: '12px',
          border: '1px solid #e2e8f0',
          alignSelf: 'start',
          position: 'sticky',
          top: '80px'
        }}>
          <h2 style={{ marginBottom: '1rem' }}>Zusammenfassung</h2>
          <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
            <span>Zwischensumme</span>
            <span>{totalPrice.toFixed(2)} €</span>
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
            <span>Versand</span>
            <span>{totalPrice >= 50 ? 'Kostenlos' : '4,99 €'}</span>
          </div>
          <hr style={{ border: 'none', borderTop: '1px solid #e2e8f0', margin: '1rem 0' }} />
          <div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700, fontSize: '1.2rem' }}>
            <span>Gesamt</span>
            <span>{(totalPrice + (totalPrice >= 50 ? 0 : 4.99)).toFixed(2)} €</span>
          </div>
          {totalPrice < 50 && (
            <p style={{ fontSize: '0.8rem', color: '#64748b', marginTop: '0.5rem' }}>
              Noch {(50 - totalPrice).toFixed(2)} € bis zum kostenlosen Versand!
            </p>
          )}
          <Link to="/checkout" style={{
            display: 'block',
            width: '100%',
            padding: '12px',
            marginTop: '1.5rem',
            backgroundColor: '#2563eb',
            color: 'white',
            textAlign: 'center',
            textDecoration: 'none',
            borderRadius: '8px',
            fontWeight: 600
          }}>
            Zur Kasse
          </Link>
        </div>
      </div>
    </div>
  );
}

export default Cart;

Schritt 7: App zusammensetzen

// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { CartProvider } from './context/CartContext';
import Layout from './components/Layout';
import Home from './pages/Home';
import Cart from './pages/Cart';

function App() {
  return (
    <BrowserRouter>
      <CartProvider>
        <Routes>
          <Route path="/" element={<Layout />}>
            <Route index element={<Home />} />
            <Route path="cart" element={<Cart />} />
          </Route>
        </Routes>
      </CartProvider>
    </BrowserRouter>
  );
}

export default App;

Uebungen zur Erweiterung

  1. Erstelle eine Produktdetail-Seite mit useParams, die das vollstaendige Produkt anzeigt
  2. Fuege eine Checkout-Seite mit Liefer- und Zahlungsformular hinzu
  3. Implementiere eine Wunschliste als zusaetzlichen Context
  4. Speichere den Warenkorb im localStorage, damit er nach dem Reload erhalten bleibt
  5. Lade Produkte von einer API statt aus der statischen Datei

Was kommt als Naechstes?

Du hast einen funktionierenden Online-Shop gebaut! Im naechsten und letzten Projekt erstellst du eine Dashboard-App – das komplexeste Projekt in diesem Kurs.

Zusammenfassung

  • Cart-Context mit useReducer verwaltet den Warenkorb-State zentral
  • Produktfilter kombinieren Suche, Kategorie und Sortierung mit useMemo
  • React Router ermoeglicht Navigation zwischen Produktliste, Warenkorb und Checkout
  • Komponentenaufteilung haelt den Code uebersichtlich und wartbar
  • Berechnete Werte (Gesamtpreis, Versandkosten) werden aus dem State abgeleitet

Pro-Tipp: In einem echten Online-Shop wuerdest du Produktdaten von einer API laden, Zahlungen ueber einen Service wie Stripe abwickeln und den Warenkorb serverseitig validieren. Dieses Projekt gibt dir das Frontend-Fundament – die Backend-Integration kannst du als naechsten Lernschritt angehen!

Zurück zum React Kurs