Zum Inhalt springen

Dynamische Routen & Parameter

Die Todo-Liste zeigt alle Einträge. Ein Klick auf einen Eintrag soll eine Detailansicht öffnen — mit einer eigenen URL pro Todo: /todos/1, /todos/42. Das ist eine dynamische Route: der Pfad hat einen variablen Teil, den React Router als Parameter bereitstellt.

Ein Doppelpunkt im Pfad markiert einen Parameter:

main.tsx
import TodoDetail from './TodoDetail'
const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{ index: true, element: <TodoListe /> },
{ path: 'aktiv', element: <TodoAktiv /> },
{ path: 'erledigt', element: <TodoErledigt /> },
{ path: 'todos/:id', element: <TodoDetail /> },
],
},
])

:id ist ein Platzhalter — React Router matcht /todos/1, /todos/42 und jeden anderen Wert an dieser Stelle und macht ihn als id verfügbar.

import { useParams, Link } from 'react-router'
import { useQuery } from '@tanstack/react-query'
type Todo = { id: number; title: string; completed: boolean; userId: number }
async function fetchTodo(id: number): Promise<Todo> {
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
if (!res.ok) throw new Error(`HTTP-Fehler: ${res.status}`)
return res.json()
}
export default function TodoDetail() {
const { id } = useParams<{ id: string }>()
const numericId = parseInt(id!, 10)
const { data: todo, isLoading, isError } = useQuery<Todo>({
queryKey: ['todo', numericId],
queryFn: () => fetchTodo(numericId),
})
if (isLoading) return <p>Lade...</p>
if (isError) return <p>Fehler beim Laden.</p>
if (!todo) return null
return (
<div>
<Link to="/">← Zurück</Link>
<h2>{todo.title}</h2>
<p>Status: {todo.completed ? 'Erledigt' : 'Offen'}</p>
<p>Nutzer: {todo.userId}</p>
</div>
)
}

useParams<{ id: string }>() gibt { id: string | undefined } zurück. Das ! nach id teilt TypeScript mit, dass der Wert hier vorhanden ist — was stimmt, solange die Komponente nur innerhalb der :id-Route verwendet wird.

In der Liste verlinkt jeder Eintrag auf seine Detailroute:

TodoListe.tsx
export default function TodoListe() {
const { data: todos, isLoading } = useQuery<Todo[]>({
queryKey: ['todos'],
queryFn: fetchTodos,
})
if (isLoading) return <p>Lade...</p>
return (
<ul>
{todos!.map(todo => (
<li key={todo.id}>
<Link to={`/todos/${todo.id}`}>{todo.title}</Link>
</li>
))}
</ul>
)
}

Wenn jemand /todos/99999 aufruft und die API einen 404 zurückgibt, wirft fetchTodo einen Fehler — isError wird true und die Fehlermeldung wird angezeigt. Das ist ausreichend für viele Fälle.

Für einen dedizierten 404-Catch auf Router-Ebene gibt es errorElement:

const router = createBrowserRouter([
{
path: '/',
element: <App />,
errorElement: <NichtGefunden />,
children: [
// ...
],
},
])
NichtGefunden.tsx
import { Link, useRouteError, isRouteErrorResponse } from 'react-router'
export default function NichtGefunden() {
const error = useRouteError()
return (
<div>
<h2>Seite nicht gefunden</h2>
{isRouteErrorResponse(error) && <p>Status: {error.status}</p>}
<Link to="/">Zur Startseite</Link>
</div>
)
}

errorElement fängt zwei Dinge: Fehler, die in Route-Komponenten geworfen werden, und Navigationen zu Pfaden, die keiner Route entsprechen.

useParams gibt string | undefined für jeden Parameter zurück — nicht string. Drei Strategien:

const { id } = useParams<{ id: string }>()
// 1. Early Exit — sauberste Lösung
if (!id) return null
const numericId = parseInt(id, 10)
// 2. Non-null assertion — wenn sicher, dass die Route den Param immer hat
const numericId = parseInt(id!, 10)
// 3. Fallback
const numericId = parseInt(id ?? '0', 10)

Der Early Exit ist bevorzugt: er macht den unmöglichen Fall explizit, ohne TypeScript zu überlisten.