Zum Inhalt springen

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.

Terminal-Fenster
pnpm add react-router

React Router v7 exportiert alles aus einem einzigen Paket — kein separates react-router-dom mehr.

Die App-Struktur wird einmal zentral definiert: welcher Pfad welche Komponente rendert.

main.tsx
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.

App.tsx
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.

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.

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”.

Mit TanStack Query lädt jede Route-Komponente ihre eigenen Daten:

TodoListe.tsx
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>
)
}
TodoAktiv.tsx
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.

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 null
const numericId = parseInt(id, 10)

Im nächsten Kapitel bauen wir genau das aus: eine Detailansicht mit einem :id-Parameter.