Routing Grundlagen
Bisher bestand unsere App aus einer einzigen Ansicht. Eine echte Anwendung hat mehrere Seiten: eine Liste, eine Detailansicht, eine Einstellungsseite. React selbst kennt kein Routing — das übernimmt eine externe Bibliothek. Die am weitesten verbreitete ist React Router.
Installation
Abschnitt betitelt „Installation“pnpm add react-routerReact Router v7 exportiert alles aus einem einzigen Paket — kein separates react-router-dom mehr.
Router definieren
Abschnitt betitelt „Router definieren“Die App-Struktur wird einmal zentral definiert: welcher Pfad welche Komponente rendert.
import { StrictMode } from 'react'import { createRoot } from 'react-dom/client'import { createBrowserRouter, RouterProvider } from 'react-router'import App from './App'import TodoListe from './TodoListe'import TodoAktiv from './TodoAktiv'import TodoErledigt from './TodoErledigt'
const router = createBrowserRouter([ { path: '/', element: <App />, children: [ { index: true, element: <TodoListe /> }, { path: 'aktiv', element: <TodoAktiv /> }, { path: 'erledigt', element: <TodoErledigt /> }, ], },])
createRoot(document.getElementById('root')!).render( <StrictMode> <RouterProvider router={router} /> </StrictMode>)App ist das Layout — es enthält Navigation und einen Platzhalter (<Outlet />), in den React Router die aktive Child-Route rendert.
Layout mit Outlet
Abschnitt betitelt „Layout mit Outlet“import { NavLink, Outlet } from 'react-router'
export default function App() { return ( <div> <h1>Todos</h1> <nav> <NavLink to="/" end>Alle</NavLink> <NavLink to="/aktiv">Aktiv</NavLink> <NavLink to="/erledigt">Erledigt</NavLink> </nav> <Outlet /> </div> )}<Outlet /> ist der Slot, in den die aktive Child-Route gerendert wird. Navigiert der Nutzer zu /aktiv, ersetzt React Router den Inhalt des Outlets mit <TodoAktiv /> — ohne die App-Komponente neu zu mounten.
NavLink vs. Link
Abschnitt betitelt „NavLink vs. Link“Beide erzeugen ein <a>-Element, das die Browser-URL ändert, ohne die Seite neu zu laden:
import { Link, NavLink } from 'react-router'
// Link — einfach<Link to="/aktiv">Aktiv</Link>
// NavLink — fügt automatisch die Klasse "active" hinzu, wenn die Route aktiv ist<NavLink to="/aktiv">Aktiv</NavLink>
// NavLink mit eigenem Styling<NavLink to="/aktiv" style={({ isActive }) => ({ fontWeight: isActive ? 'bold' : 'normal' })}> Aktiv</NavLink>NavLink eignet sich für Navigationsleisten — die aktive Route ist über CSS oder Inline-Style direkt erkennbar. Das end-Prop bei to="/" verhindert, dass der Root-Link immer als aktiv gilt, wenn eine Sub-Route aktiv ist.
useNavigate: programmatisch navigieren
Abschnitt betitelt „useNavigate: programmatisch navigieren“Für Navigation als Reaktion auf eine Aktion — z.B. nach dem Absenden eines Formulars:
import { useNavigate } from 'react-router'
export function TodoFormular() { const navigate = useNavigate()
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() // ... Todo speichern navigate('/') // zurück zur Liste }
return ( <form onSubmit={handleSubmit}> {/* ... */} </form> )}navigate(-1) geht einen Schritt zurück in der Browser-History — entspricht dem Klick auf “Zurück”.
Die Route-Komponenten
Abschnitt betitelt „Die Route-Komponenten“Mit TanStack Query lädt jede Route-Komponente ihre eigenen Daten:
import { useQuery } from '@tanstack/react-query'
type Todo = { id: number; title: string; completed: boolean; userId: number }
async function fetchTodos(): Promise<Todo[]> { const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=20') if (!res.ok) throw new Error(`HTTP-Fehler: ${res.status}`) return res.json()}
export default function TodoListe() { const { data: todos, isLoading, isError } = useQuery<Todo[]>({ queryKey: ['todos'], queryFn: fetchTodos, })
if (isLoading) return <p>Lade...</p> if (isError) return <p>Fehler beim Laden.</p>
return ( <ul> {todos!.map(todo => ( <li key={todo.id}> <Link to={`/todos/${todo.id}`}>{todo.title}</Link> </li> ))} </ul> )}export default function TodoAktiv() { const { data: todos, isLoading } = useQuery<Todo[]>({ queryKey: ['todos'], queryFn: fetchTodos, })
if (isLoading) return <p>Lade...</p>
const aktive = todos?.filter(t => !t.completed) ?? []
return ( <ul> {aktive.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> )}Da queryKey: ['todos'] in beiden Komponenten identisch ist, greift TanStack Query auf denselben Cache-Eintrag zurück — die Daten werden nur einmal geladen, egal wie oft zwischen den Routen gewechselt wird.
TypeScript
Abschnitt betitelt „TypeScript“useParams gibt Record<string, string | undefined> zurück — alle Params sind optional, weil TypeScript nicht wissen kann, welche Params die aktuelle Route hat. Mit einem Generic wird der Typ explizit:
import { useParams } from 'react-router'
const { id } = useParams<{ id: string }>()id ist dann string | undefined — nicht string. Das erzwingt eine Prüfung vor der Verwendung:
if (!id) return nullconst numericId = parseInt(id, 10)Im nächsten Kapitel bauen wir genau das aus: eine Detailansicht mit einem :id-Parameter.