Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

React Router Basics: Multi-Page React Apps

React Router Basics

~12 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Welcome to Phase 4: so far our app has had just a single "page". A real website needs multiple URLs, though – a product list, a detail page, a login area. React itself has no built-in system for this – we install the standard library React Router.

Installation

npm install react-router-dom

App.jsx is getting messy – time to split it up

Our App.jsx now contains search, statistics, AND a review form on ONE single page. Right now, when we need several real sub-pages, is exactly the moment to move the current product list part out into its own file src/pages/ProductListPage.jsx.

Creating src/pages/ProductListPage.jsx

Create a new folder src/pages/ with this file:

src/pages/ProductListPage.jsx
import { useMemo, useRef, useState } from 'react';
import ProductCard from '../components/ProductCard';

const SAMPLE_PRODUCTS = [
  { sku: 'boots-01', name: 'Hiking Boots', price: 89.99, imageUrl: 'https://picsum.photos/seed/1/300' },
  { sku: 'backpack-01', name: 'Trekking Backpack', price: 59.5, imageUrl: 'https://picsum.photos/seed/2/300' },
  { sku: 'jacket-01', name: 'Rain Jacket', price: 74.0, imageUrl: 'https://picsum.photos/seed/3/300' },
];

function ProductListPage() {
  const [query, setQuery] = useState('');
  const searchInputRef = useRef(null);

  const filtered = SAMPLE_PRODUCTS.filter((product) =>
    product.name.toLowerCase().includes(query.toLowerCase())
  );

  const mostExpensiveProduct = useMemo(() => {
    return filtered.reduce(
      (max, p) => (p.price > (max?.price ?? 0) ? p : max),
      null
    );
  }, [filtered]);

  return (
    <div>
      <input
        ref={searchInputRef}
        type="text"
        placeholder="Search products..."
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      {mostExpensiveProduct && (
        <p>Most expensive product: {mostExpensiveProduct.name} (${mostExpensiveProduct.price.toFixed(2)})</p>
      )}
      <div className="product-grid">
        {filtered.map((product) => (
          <ProductCard
            key={product.sku}
            name={product.name}
            price={product.price}
            imageUrl={product.imageUrl}
            onSelect={() => alert(product.name + ' tapped')}
          />
        ))}
      </div>
    </div>
  );
}

export default ProductListPage;

Notice: no more useDocumentTitle, no more useAuth, no more <header> here – those belong in App.jsx, which from now on acts as a shared "layout" around ALL pages, no longer as the one page itself.

Cleaning up App.jsx: router and layout

React Router distinguishes three central building blocks: <BrowserRouter> (enables routing at all, goes in main.jsx), <Routes>/<Route> (define which URL shows which page), and <Link> (React Router's equivalent of <a href="..."> that does NOT reload the page).

src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.jsx';
import { AuthProvider } from './context/AuthContext.jsx';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter>
      <AuthProvider>
        <App />
      </AuthProvider>
    </BrowserRouter>
  </StrictMode>
);
src/App.jsx
import { Routes, Route } from 'react-router-dom';
import { useAuth } from './context/AuthContext';
import { useDocumentTitle } from './hooks/useDocumentTitle';
import ProductListPage from './pages/ProductListPage';

function App() {
  const { user, login, logout } = useAuth();
  useDocumentTitle('Product Catalog');

  return (
    <div className="app">
      <header className="app-header">
        <h1>Product Catalog</h1>
        {user ? (
          <p>Logged in as {user.username} <button onClick={logout}>Log out</button></p>
        ) : (
          <button onClick={() => login('Jane Doe')}>Log in</button>
        )}
      </header>

      <Routes>
        <Route path="/" element={<ProductListPage />} />
      </Routes>
    </div>
  );
}

export default App;

Project structure after this chapter

produktkatalog-web/
├── index.html
├── package.json
├── vite.config.js
└── src/
    ├── main.jsx                     (updated: BrowserRouter)
    ├── App.jsx                      (cleaned up: layout + Routes)
    ├── index.css
    ├── context/
    │   └── AuthContext.jsx
    ├── hooks/
    │   └── useDocumentTitle.js
    ├── components/
    │   ├── ProductCard.jsx
    │   └── ReviewForm.jsx
    └── pages/
        └── ProductListPage.jsx      ← NEW (content from old App.jsx)

<Routes> looks at the current URL and renders exactly the <Route> whose path matches – path="/" represents the home page. Everything outside <Routes> (here: <header>) stays visible unchanged across every page switch – that's exactly what makes a "layout".

Achtung: The review form part from chapter 16 is now deliberately missing – we'll build it back in as its own ProductDetailPage.jsx sub-page, reachable via a dynamic route, in the next chapter, tied to a specific product instead of loose on the home page.