Routing
Kurz gesagt: Routing verbindet die Adresse mit der Seite: React Router zeigt die Komponente der Route, Links laden nicht neu, und Parameter wie /product/:id kommen aus der Adresse.
Problem
Ein Link, der ins Leere führt
Der Shop hat einen Katalog und eine Seite für jedes Produkt. Öffne „Frischmilch“ und geh zurück.
Konsole
Es sieht gut aus. Aber welche Seite du siehst, steckt nur im Zustand der Komponente (productId). Die Adresse im Browser ist für den Katalog und für jedes Produkt dieselbe. Deshalb:
- du kannst einem Freund keinen Link zur Milch schicken: Er bekommt den Katalog;
- „Neu laden“ bringt dich zurück zum Katalog, weil der Zustand verloren ist;
- „Zurück“ im Browser führt nicht zum Katalog, sondern aus dem Shop heraus;
- die Produkte sind Buttons, keine Links: „In neuem Tab öffnen“ funktioniert nicht.
(Die Sandbox hat keine Adressleiste, deshalb zeigt sich das in ihr nicht. Auf einer echten Seite siehst du es sofort.)
Die Idee
Die Adresse ist der Zustand
Jede Seite braucht ihre eigene Adresse: / für den Katalog, /product/5 für die Milch, /cart für den Warenkorb. Dann sagt die Adresse, welche Seite gezeigt wird, nicht der Zustand im Speicher.
Die App bleibt eine Seite (eine Single-Page-Anwendung): Der Browser lädt das HTML einmal. Bei einem Klick auf einen Link hält die Bibliothek das Neuladen an, ändert die Adresse über die History-API des Browsers und zeichnet eine andere Komponente. Das ist die Aufgabe des Routers. Er hat eine Routentabelle: ein Adressmuster und eine Komponente dafür.
React Router liefert Komponenten für die Tabelle. Jede Route sagt: „Für diese Adresse zeichne dieses Element“:
import { MemoryRouter, Routes, Route } from 'react-router';
export default function App() { return ( <MemoryRouter> <Header /> <Routes> <Route path="/" element={<Catalog />} /> <Route path="/product/:id" element={<ProductPage />} /> <Route path="/cart" element={<CartPage />} /> </Routes> </MemoryRouter> );}Routes zeichnet nur eine seiner Routen: die, die zur Adresse passt. Header steht außerhalb von Routes, deshalb erscheint er auf jeder Seite.
:id in /product/:id ist ein Parameter: Er steht für ein Segment der Adresse. Eine Route deckt alle Produkte ab.
Sieh es dir an
Sieh, wie die Adresse die Seite wählt
<Link to={`/product/${p.id}`}>{p.name}</Link>
<Routes> <Route path="/" element={<Catalog />} /> <Route path="/product/:id" element={<ProductPage />} /> <Route path="/cart" element={<CartPage />} /> <Route path="*" element={<NotFound />} /></Routes>
function ProductPage() { const { id } = useParams(); const product = PRODUCTS.find((p) => p.id === Number(id)); return <h1>{product.name}</h1>;}
function NotFound() { return <h1>Diese Seite gibt es nicht</h1>;}Schritt für Schritt, in Worten
- Der Router hat eine Tabelle: ein Adressmuster und die Komponente dafür. Die Adresse ist jetzt /, der Katalog.
- Ein Klick auf „Frischmilch“. Der Router verhindert das Neuladen der Seite und ändert nur die Adresse zu /product/5.
- Er vergleicht die Adresse mit jeder Zeile. „/product/:id“ passt: :id steht für ein beliebiges Segment. Die Zeile für alles andere passt auch, ist aber allgemeiner und verliert.
- Aus dem Treffer kommt der Parameter: id = "5". Er ist Text, weil die Adresse Text ist. useParams() gibt ihn an ProductPage, und Number(id) macht daraus 5.
- ProductPage findet Produkt 5 und zeichnet seine Seite. Dieselbe Komponente zeichnet jedes Produkt: Nur der Parameter ist anders.
- Jetzt ist die Adresse /sale, und diese Seite gibt es noch nicht. Keine Zeile passt außer der letzten, also zeichnet der Router NotFound.
- Die Adresse ist der Zustand der Seite. Du kannst sie teilen, neu laden und zu ihr zurückkehren, und die Tabelle liefert immer dieselbe Seite.
Der Router weiß nichts über Produkte. Er vergleicht den Text der Adresse mit den Mustern und reicht den Parameter an die Komponente weiter. Was 5 bedeutet, entscheidet die Komponente.
Die Idee
In der Sandbox und in einem echten Projekt
Der Router einer echten Seite liest und schreibt die Adresse in der Adressleiste des Browsers. Das ist BrowserRouter. Die Sandbox in den Lektionen ist eine Seite in der Lektionsseite und hat keine eigene Adresse. Deshalb gibt es hier einen klaren Fehler:
Konsole
In der Sandbox nehmen wir MemoryRouter: einen Router, der die Adresse im Speicher hält. Alles andere ist gleich: die Tabelle, die Links, die Parameter. Die Adresse zeigen wir selbst, unten auf der Seite.
In einem echten Projekt umschließt der Router die App in main.jsx:
import { createRoot } from 'react-dom/client';import { BrowserRouter } from 'react-router';import App from './App.jsx';
createRoot(document.getElementById('root')).render( <BrowserRouter> <App /> </BrowserRouter>,);Und noch etwas, das die Sandbox nicht zeigt: der Server. Wenn jemand /product/5 direkt öffnet oder auf „Neu laden“ klickt, fragt der Browser den Server nach dieser Adresse. Eine solche Datei gibt es nicht. Der Server muss für jede Adresse, die keine Datei ist, index.html liefern, und dann wählt der Router die Seite. Der Vite-Entwicklungsserver macht das von selbst. Bei nginx erledigt es die Zeile try_files:
location / { try_files $uri /index.html;}Probier es aus
Eine Seite für jedes Produkt
Die Tabelle hat den Katalog und den Warenkorb, und die Produkte führen zu /product/1, /product/5 und /product/7, aber eine solche Route gibt es nicht. Füge für die Produktseite eine Route mit Parameter hinzu. ProductPage zeigt jetzt immer das Brot: Lass sie die id aus der Adresse nehmen. Die Prüfung öffnet die Milch, dann den Käse unter „Siehe auch“, dann das Brot aus dem Katalog und sieht sich die Überschrift und die Adresse an.
Konsole
Lösung
App.jsximport { useState } from 'react';
import { MemoryRouter, Routes, Route } from 'react-router';
import Header from './Header.jsx';
import Catalog from './Catalog.jsx';
import ProductPage from './ProductPage.jsx';
import CartPage from './CartPage.jsx';
import Address from './Address.jsx';
export default function App() {
const [items, setItems] = useState([]);
function add(product) {
setItems([...items, product]);
}
return (
<MemoryRouter>
<Header count={items.length} />
<main>
<Routes>
<Route path="/" element={<Catalog />} />
<Route path="/product/:id" element={<ProductPage onAdd={add} />} />
<Route path="/cart" element={<CartPage items={items} />} />
</Routes>
</main>
<Address />
</MemoryRouter>
);
}
Header.jsximport { NavLink } from 'react-router';
export default function Header({ count }) {
return (
<nav>
<NavLink to="/" end>Katalog</NavLink>
<NavLink to="/cart">Warenkorb ({count})</NavLink>
</nav>
);
}
Catalog.jsximport { Link } from 'react-router';
import { PRODUCTS, money } from './products.js';
export default function Catalog() {
return (
<>
<h1>Katalog</h1>
<ul>
{PRODUCTS.map((p) => (
<li key={p.id}>
<Link to={`/product/${p.id}`}>{p.name}</Link>, {money(p.price)}
</li>
))}
</ul>
</>
);
}
ProductPage.jsximport { Link, useParams } from 'react-router';
import { PRODUCTS, money } from './products.js';
export default function ProductPage({ onAdd }) {
const { id } = useParams();
const product = PRODUCTS.find((p) => p.id === Number(id));
if (!product) return <p>Kein solches Produkt</p>;
const others = PRODUCTS.filter((p) => p.id !== product.id);
function buy() {
onAdd(product);
}
return (
<section>
<h1>{product.name}</h1>
<p>{money(product.price)}</p>
<button type="button" onClick={buy}>In den Warenkorb</button>
<h2>Siehe auch</h2>
<ul>
{others.map((p) => (
<li key={p.id}>
<Link to={`/product/${p.id}`}>{p.name}</Link>
</li>
))}
</ul>
</section>
);
}
CartPage.jsximport { money } from './products.js';
export default function CartPage({ items }) {
const total = items.reduce((sum, p) => sum + p.price, 0);
return (
<>
<h1>Warenkorb</h1>
{items.length === 0 && <p>Der Warenkorb ist leer</p>}
<ul>
{items.map((p, i) => (
<li key={i}>{p.name}, {money(p.price)}</li>
))}
</ul>
<p>Gesamt: {money(total)}</p>
</>
);
}
NotFound.jsximport { Link } from 'react-router';
export default function NotFound() {
return (
<>
<h1>Diese Seite gibt es nicht</h1>
<p>
<Link to="/">Zurück zum Katalog</Link>
</p>
</>
);
}
Address.jsximport { useLocation } from 'react-router';
// Die Sandbox hat keine Adressleiste: Wir zeigen die Adresse hier.
export default function Address() {
const location = useLocation();
return <footer>Adresse: {location.pathname}</footer>;
}
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
nav { display: flex; gap: 16px; background: #f3efe6; padding: 8px 16px; }
nav a { color: #1d4f8a; }
nav a[aria-current='page'] { font-weight: 700; text-decoration: none; }
main { padding: 0 16px; }
footer { border-top: 1px solid #ddd; padding: 4px 16px; color: #555; font-family: ui-monospace, monospace; }
button { font: inherit; }
In App.jsx ist die Route <Route path="/product/:id" element={<ProductPage onAdd={add} />} />. In ProductPage.jsx kommt der Parameter aus const { id } = useParams();. Der Name id ist derselbe wie im Muster :id.
Der Parameter ist Text: "5", während id in den Daten eine Zahl ist. p.id === "5" ist für jedes Produkt false. Vergleiche mit Number(id).
Probier es aus
Finde den Fehler
Jede Produktseite sagt „Kein solches Produkt“, und die Produkte sind in den Daten. Welche Zeile ist schuld?
Wähle die Zeile mit dem Fehler.
id aus useParams() ist Text ("5"), und p.id ist eine Zahl. Das strikte === ist nie wahr. Es muss p.id === Number(id) heißen.Die Idee
Links und Navigation aus dem Code
Links. Eine Seite der App erreichst du mit <Link to="/cart">. In der Seite ist es ein normales <a href="/cart">: „In neuem Tab öffnen“ funktioniert, und ein Screenreader liest es als Link. Bei einem normalen Klick hält der Router das Neuladen an (preventDefault) und ändert nur die Adresse. Schreibst du <a href> selbst, lädt der Browser die ganze Seite noch einmal, und der Warenkorb im Speicher ist weg.
Für das Menü gibt es NavLink: Der Link zur aktuellen Seite bekommt aria-current="page" und kann einen eigenen Stil haben. So wissen das Auge und der Screenreader, wo du bist.
Navigation aus dem Code. Manchmal muss die Seite nach einer Aktion wechseln und nicht nach einem Klick auf einen Link: „In den Warenkorb“ legt das Produkt hinein und bringt dich zum Warenkorb.
import { useNavigate } from 'react-router';
export default function ProductPage({ onAdd }) { const navigate = useNavigate(); // … function buy() { onAdd(product); navigate('/cart'); }}useNavigate() ist ein Hook: Du rufst ihn im Rumpf der Komponente auf und bekommst eine Funktion navigate. Die Funktion selbst rufst du dort auf, wo du sie brauchst, zum Beispiel im Handler.
Einen Link benutzt du, wenn ein Mensch wählt, wohin er geht. Aus dem Code navigierst du, wenn die Seite wegen des Ergebnisses einer Aktion wechselt.
Probier es aus
Ergänze
Woher bekommt die Produktseite die id aus der Adresse?
export default function ProductPage() {
const { id } = ();
const product = PRODUCTS.find((p) => p.id === Number(id));
return <h1>{product.name}</h1>;
}useParams: gibt ein Objekt mit den Parametern der passenden Route zurück. Für /product/:id ist das { id: "5" }.Die Idee
Diese Seite gibt es nicht
Wenn die Adresse zu keiner Route passt, zeichnet der Router nichts. Der Mensch sieht eine leere Fläche unter dem Menü und weiß nicht, was passiert ist. Deshalb fängt die letzte Route alles andere ab und zeichnet „Diese Seite gibt es nicht“ mit einem Link zum Katalog.
<Route path="*" element={<NotFound />} />* passt auf jede Adresse. Die Reihenfolge in Routes spielt keine Rolle: React Router ordnet die Routen danach, wie genau sie sind, und * gewinnt nur, wenn keine andere Route passt.
Eine Seite für ein Produkt, das es nicht gibt (/product/99), ist ein anderer Fall: Die Route passt, aber das Produkt fehlt. Das entscheidet die Komponente, also sagt ProductPage „Kein solches Produkt“.
Genauer, für Neugierige
Verschachtelte Routen. Seiten teilen oft einen Rahmen: das Konto mit einem Menü links und den Unterseiten „Bestellungen“ und „Adressen“. Routen werden ineinander verschachtelt, und das Elternelement zeichnet <Outlet /> dort, wo das Kind hingehört.
Eine Query in der Adresse. Suche und Filter sind ebenfalls Zustand, den man gut in die Adresse legt: /?q=milch&category=2. Sie wird mit useSearchParams() gelesen und geschrieben.
Dieselbe Komponente, ein neuer Parameter. Von /product/5 zu /product/7 rendert React dieselbe ProductPage noch einmal mit einer neuen id, deshalb stimmen die Berechnungen im Rumpf. Aber ihr Zustand (etwa ein Feld „Menge“) bleibt vom vorigen Produkt erhalten. Soll er von vorn beginnen, gib ihr einen Schlüssel: <ProductPage key={id} /> in einer Komponente, die id liest.
Die Modi von React Router. Hier ist es der „deklarative“ Modus: Routes und Route in JSX. Es gibt auch einen Datenmodus (createBrowserRouter mit einem loader, der Daten vor der Seite lädt) und einen „Framework“-Modus mit Vite. Die Idee von Tabelle, Parametern und Links ist dieselbe.
Guards. Die Seite „Zur Kasse“ kann eine Anmeldung verlangen. Der Router kann vor dem Wechsel prüfen und zur Anmeldung umleiten: im Datenmodus mit einem loader und redirect.
Übung
Der Warenkorb und 404
Das Menü hat „Angebote“ (/sale), aber diese Seite gibt es noch nicht, und die Fläche unter dem Menü bleibt leer. Und „In den Warenkorb“ legt das Produkt hinein, aber der Mensch bleibt auf der Produktseite. Füge eine Route für alles andere zu NotFound hinzu (die Komponente ist fertig) und lass den Button nach dem Hinzufügen zu /cart gehen. Die Prüfung öffnet „Angebote“, geht mit „Zurück zum Katalog“ zurück, kauft Brot und Käse und sieht sich den Warenkorb, die Adresse und die Summe an.
Konsole
Lösung
App.jsximport { useState } from 'react';
import { MemoryRouter, Routes, Route } from 'react-router';
import Header from './Header.jsx';
import Catalog from './Catalog.jsx';
import ProductPage from './ProductPage.jsx';
import CartPage from './CartPage.jsx';
import NotFound from './NotFound.jsx';
import Address from './Address.jsx';
export default function App() {
const [items, setItems] = useState([]);
function add(product) {
setItems([...items, product]);
}
return (
<MemoryRouter>
<Header count={items.length} />
<main>
<Routes>
<Route path="/" element={<Catalog />} />
<Route path="/product/:id" element={<ProductPage onAdd={add} />} />
<Route path="/cart" element={<CartPage items={items} />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
<Address />
</MemoryRouter>
);
}
Header.jsximport { NavLink } from 'react-router';
export default function Header({ count }) {
return (
<nav>
<NavLink to="/" end>Katalog</NavLink>
<NavLink to="/cart">Warenkorb ({count})</NavLink>
<NavLink to="/sale">Angebote</NavLink>
</nav>
);
}
Catalog.jsximport { Link } from 'react-router';
import { PRODUCTS, money } from './products.js';
export default function Catalog() {
return (
<>
<h1>Katalog</h1>
<ul>
{PRODUCTS.map((p) => (
<li key={p.id}>
<Link to={`/product/${p.id}`}>{p.name}</Link>, {money(p.price)}
</li>
))}
</ul>
</>
);
}
ProductPage.jsximport { Link, useNavigate, useParams } from 'react-router';
import { PRODUCTS, money } from './products.js';
export default function ProductPage({ onAdd }) {
const { id } = useParams();
const navigate = useNavigate();
const product = PRODUCTS.find((p) => p.id === Number(id));
if (!product) return <p>Kein solches Produkt</p>;
const others = PRODUCTS.filter((p) => p.id !== product.id);
function buy() {
onAdd(product);
navigate('/cart');
}
return (
<section>
<h1>{product.name}</h1>
<p>{money(product.price)}</p>
<button type="button" onClick={buy}>In den Warenkorb</button>
<h2>Siehe auch</h2>
<ul>
{others.map((p) => (
<li key={p.id}>
<Link to={`/product/${p.id}`}>{p.name}</Link>
</li>
))}
</ul>
</section>
);
}
CartPage.jsximport { money } from './products.js';
export default function CartPage({ items }) {
const total = items.reduce((sum, p) => sum + p.price, 0);
return (
<>
<h1>Warenkorb</h1>
{items.length === 0 && <p>Der Warenkorb ist leer</p>}
<ul>
{items.map((p, i) => (
<li key={i}>{p.name}, {money(p.price)}</li>
))}
</ul>
<p>Gesamt: {money(total)}</p>
</>
);
}
NotFound.jsximport { Link } from 'react-router';
export default function NotFound() {
return (
<>
<h1>Diese Seite gibt es nicht</h1>
<p>
<Link to="/">Zurück zum Katalog</Link>
</p>
</>
);
}
Address.jsximport { useLocation } from 'react-router';
// Die Sandbox hat keine Adressleiste: Wir zeigen die Adresse hier.
export default function Address() {
const location = useLocation();
return <footer>Adresse: {location.pathname}</footer>;
}
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
nav { display: flex; gap: 16px; background: #f3efe6; padding: 8px 16px; }
nav a { color: #1d4f8a; }
nav a[aria-current='page'] { font-weight: 700; text-decoration: none; }
main { padding: 0 16px; }
footer { border-top: 1px solid #ddd; padding: 4px 16px; color: #555; font-family: ui-monospace, monospace; }
button { font: inherit; }
Die Route ist <Route path="*" element={<NotFound />} />. In ProductPage.jsx: const navigate = useNavigate(); im Rumpf und navigate('/cart') in buy nach onAdd(product). Der Warenkorb liegt in App, deshalb sehen die Anzahl im Menü und CartPage ihn sofort.
Die Prüfung sieht sich auch das Menü an: Auf der Warenkorbseite hat der Link „Warenkorb (1)“ aria-current="page". Das macht der Router von selbst.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Zustand · Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Gemeinsamer Zustand
- Geht weiter in: Geschwindigkeit: wann eine Komponente neu rendert · Von der Sandbox zum Projekt · Das Schaufenster des Shops
- Aufgaben zum Thema: Eine ganze App