Typsicheres Routing in React
Routing in React war lange eine Quelle von Laufzeitfehlern: falsche URL-Parameter, untypisierte Search Params, fehlende Routen-Guards. TanStack Router löst das durch ein vollständig typsicheres Routing-System, bei dem der TypeScript-Compiler jeden Fehler in Navigation, Params und Search Params erkennt – bevor der Code den Browser erreicht.
Inhaltsverzeichnis
- 1. Das Routing-Problem in TypeScript-React-Apps
- 2. Grundprinzip: Routen als typisierte Objekte
- 3. File-based Routing mit Vite-Plugin
- 4. Route Params: typsicher aus der URL lesen
- 5. Search Params: strukturierte URL-Zustände
- 6. Loaders: Datenabruf vor dem Rendern
- 7. Typsichere Navigation mit Link und navigate
- 8. Code Splitting und lazy loading
- 9. TanStack Router vs. React Router v6 im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Routing-Problem in TypeScript-React-Apps
Das grundlegende Problem mit traditionellen React-Router-Lösungen und TypeScript liegt in der Typenlosigkeit der URL. Ein Link zu /products/42/edit ist ein String – TypeScript weiß nicht, ob die Route existiert, ob 42 die richtige Param-Position ist oder ob ein erforderlicher Search Param fehlt. Der Fehler tritt zur Laufzeit auf, oft in der Produktion, wenn ein Nutzer auf einen Link klickt, der zu einer nicht existierenden Route führt. TanStack Router ändert das fundamental: Routes sind typisierte Objekte, und das TypeScript-Typsystem kennt jeden Param, jeden Search Param und jede verschachtelte Route.
Ein weiteres häufiges Problem ist der unstrukturierte Umgang mit Search Params. In React Router v6 liest man Search Params mit useSearchParams(), das ein URLSearchParams-Objekt zurückgibt – ohne Typen, ohne Validierung, ohne Standardwerte. TanStack Router behandelt Search Params als ersten Bürger: Jede Route definiert ihr Search Param Schema mit einem Validator (Zod, Valibot oder custom), und alle Lese- und Schreiboperationen auf Search Params sind vollständig typisiert. Search Params werden zu einem reaktiven, typsicheren Teil des Routenzustands.
Der dritte Schmerzpunkt ist das Fehlen von integrierten Loadern in React Router ohne Framework. Datenabrufe, die vor dem Rendern einer Route stattfinden sollen, erfordern entweder loader-Funktionen aus dem Framework-Wrapper oder komplexe Suspense-Setups. TanStack Router hat eingebaute Loader, die parallel zu Elternrouten ausgeführt werden und ihre Daten typisiert an die Routenkomponente übergeben.
2. Grundprinzip: Routen als typisierte Objekte
Das Designprinzip von TanStack Router ist konsequent: Jede Route wird mit createRoute() oder createFileRoute() als typisiertes Objekt definiert. Diese Objekte werden zu einem routeTree zusammengefügt und an einen createRouter()-Aufruf übergeben. Der Typ des Routers und seines routeTree fließt durch das gesamte System: Link, navigate(), useParams() und useSearch() sind alle vom Router-Typ abgeleitet und TypeScript-geprüft.
Die Root-Route (createRootRoute()) enthält die Shell der Applikation mit <Outlet /> als Platzhalter für die aktive Kindroute. Verschachtelte Routen (createRoute({ getParentRoute: () => rootRoute })) erben den Kontext der Elternroute. Layouts entstehen durch Zwischenrouten, die selbst nur ein <Outlet /> rendern. Dieses Muster entspricht dem Nested Routing von React Router, ist aber typsicher und ohne String-basierte Routendefinitionen.
// router.tsx — typed route tree with TanStack Router
import { createRouter, createRoute, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { RootLayout } from "./layouts/RootLayout";
import { ProductsPage } from "./pages/ProductsPage";
import { ProductDetailPage } from "./pages/ProductDetailPage";
// Root route: application shell
const rootRoute = createRootRoute({
component: () => (
<>
<RootLayout />
<TanStackRouterDevtools />
</>
),
});
// /products — list route
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/products",
component: ProductsPage,
});
// /products/$productId — detail route with typed param
const productDetailRoute = createRoute({
getParentRoute: () => productsRoute,
path: "$productId",
component: ProductDetailPage,
});
// Compose the route tree
const routeTree = rootRoute.addChildren([
productsRoute.addChildren([productDetailRoute]),
]);
// Router instance — the type flows through the entire app
export const router = createRouter({ routeTree });
// Augment TanStack Router's module with our router type
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
3. File-based Routing mit Vite-Plugin
Das File-based Routing in TanStack Router funktioniert über das @tanstack/router-plugin/vite-Plugin, das ähnlich wie Next.js oder Remix arbeitet: Dateien in einem konfigurierten Verzeichnis (src/routes/) definieren automatisch Routen. Der Dateiname bestimmt den Pfad: products.tsx → /products, products.$productId.tsx → /products/$productId, products_.$productId.edit.tsx → /products/$productId/edit ohne Layout-Verschachtelung. Das Plugin generiert automatisch die routeTree.gen.ts-Datei, die alle Routentypen enthält.
In jeder Route-Datei steht createFileRoute('/products/$productId') mit dem korrekten Pfad. Der Pfad-String ist nur für die Typsicherheit nötig – das Plugin prüft, dass Dateiname und Pfad-String übereinstimmen, und gibt einen Compile-Error aus, wenn nicht. Layouts entstehen durch __layout.tsx-Dateien, die automatisch als Elternrouten für alle Dateien im selben Verzeichnis fungieren. Dieses Muster reduziert die manuelle Routenverwaltung erheblich und hält die Applikationsstruktur konsistent mit der Dateisystemstruktur.
4. Route Params: typsicher aus der URL lesen
Der häufigste Laufzeitfehler in traditionellen React-Applikationen: useParams() gibt ein Params-Objekt zurück, dessen Werte alle string | undefined sind. Jede Verwendung erfordert defensive Checks. In TanStack Router kennt useParams() dank des globalen Router-Typs genau, welche Params auf welcher Route existieren und welche Typen sie haben. Auf der productDetailRoute gibt useParams({ from: '/products/$productId' }) ein Objekt zurück, bei dem productId garantiert string ist – kein undefined, kein manueller Guard.
Param-Validierung und -Transformation erfolgen über die parseParams-Option in der Routendefinition. Dort kann der rohe String-Param in einen numerischen Wert transformiert werden: parseParams: (raw) => ({ productId: Number(raw.productId) }). Nach dieser Transformation gibt useParams() { productId: number } zurück – vollständig typisiert. Wenn der Param keine valide Zahl ist, kann die Transformationsfunktion einen Fehler werfen, den TanStack Router an die errorComponent der Route weiterleitet.
// routes/products.$productId.tsx — typed params and loader
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
// Zod schema for params validation and type inference
const paramsSchema = z.object({
productId: z.string().regex(/^\d+$/, "Must be a numeric ID").transform(Number),
});
export const Route = createFileRoute("/products/$productId")({
// Validate and transform raw URL params
parseParams: (rawParams) => paramsSchema.parse(rawParams),
// Loader runs before component render — params are fully typed here
loader: async ({ params }) => {
// params.productId is number here, not string
const res = await fetch(`/api/products/${params.productId}`);
if (!res.ok) throw new Error(`Product ${params.productId} not found`);
return res.json() as Promise<Product>;
},
component: ProductDetailPage,
errorComponent: ({ error }) => <ErrorBanner message={error.message} />,
pendingComponent: () => <ProductDetailSkeleton />,
});
function ProductDetailPage() {
// All types inferred — no casting, no undefined checks
const { productId } = Route.useParams(); // number
const product = Route.useLoaderData(); // Product
const navigate = Route.useNavigate();
return (
<div>
<h1>{product.name}</h1>
<button onClick={() => navigate({ to: "/products" })}>
Zurück zur Liste
</button>
</div>
);
}
5. Search Params: strukturierte URL-Zustände
Search Params sind in TanStack Router weit mehr als ein URLSearchParams-Wrapper. Jede Route definiert ihr Search Param Schema mit einer Validierungsfunktion, die Standardwerte, Typen und optionale Felder beschreibt. Das Schema kann Zod, Valibot oder eine einfache Transformationsfunktion sein. Wenn der Nutzer auf eine Route navigiert und bestimmte Search Params fehlen, setzt TanStack Router automatisch die im Schema definierten Standardwerte. Das eliminiert die defensive Programmierung, die bei URLSearchParams-Parsing sonst unvermeidlich ist.
Das Aktualisieren von Search Params erfolgt mit navigate({ search: (prev) => ({ ...prev, page: prev.page + 1 }) }). Der prev-Parameter ist vollständig typisiert und enthält die aktuellen Search Params der Route. Diese funktionale Update-Form verhindert, dass bestehende Search Params versehentlich überschrieben werden. In Kombination mit TanStack Query als Datenschicht entstehen elegante URL-gestützte Filter: Die Search Params sind die einzige Wahrheitsquelle für Filterkriterien, und der Query-Key enthält die Search Params direkt.
6. Loaders: Datenabruf vor dem Rendern
Loader in TanStack Router funktionieren ähnlich wie in Remix: Sie laufen vor dem Rendern der Routenkomponente und können Daten parallel zu Elternrouten abrufen. Das eliminiert Waterfalls, bei denen eine Komponente mountet, dann erst einen Fetch auslöst, auf das Ergebnis wartet und danach eine Kindkomponente rendert, die einen weiteren Fetch auslöst. Mit parallelen Loadern wird der gesamte Datenbedarf einer Route gleichzeitig aufgelöst.
Loader-Daten werden mit Route.useLoaderData() gelesen und sind vollständig typisiert aus dem Rückgabetyp der Loader-Funktion. Loader können mit context auf den Router-Kontext zugreifen, in dem TanStack Query's QueryClient bereitgestellt werden kann. Das Muster: Im Loader queryClient.ensureQueryData() aufrufen, um Cache zu befüllen oder gecachte Daten zu nutzen. Die Komponente liest dann über useSuspenseQuery() die Daten aus dem TanStack Query Cache – ohne zusätzlichen Netzwerkrequest. Beide Bibliotheken ergänzen sich hier optimal.
7. Typsichere Navigation mit Link und navigate
Der <Link>-Komponente von TanStack Router ist vollständig typisiert. Das to-Prop akzeptiert nur bekannte Routenpfade aus dem Router-Typ. Params und Search Params für die Zielroute werden als separate Props (params, search) übergeben und sind typisiert. Ein Link mit fehlendem erforderlichem Param erzeugt sofort einen TypeScript-Fehler – kein Laufzeitproblem mehr. activeProps und inactiveProps ermöglichen bedingte CSS-Klassen basierend auf dem aktiven Zustand der Route, was Breadcrumbs und Navigation stark vereinfacht.
Programmatische Navigation mit router.navigate() oder dem useNavigate()-Hook folgt derselben Typsicherheit. Das Zielobjekt mit to, params und search wird vollständig vom TypeScript-Compiler geprüft. Relative Navigation (from: '/products/$productId', to: '../') ist ebenfalls typsicher und traversiert den Route-Baum korrekt, ohne manuelle Pfadbasteleien. Das gibt dem gesamten Navigations-Code in einer React-Applikation eine Typsicherheit, die mit String-basiertem Routing prinzipiell nicht erreichbar ist.
// ProductsPage.tsx — typed Link, search params, and navigation
import { Link, useNavigate } from "@tanstack/react-router";
import { Route } from "./routes/products";
import { useProducts } from "../hooks/useProducts";
function ProductsPage() {
// Search params are typed: { category?: string; page: number; sort: "asc" | "desc" }
const { category, page, sort } = Route.useSearch();
const navigate = useNavigate({ from: "/products" });
const { data: products } = useProducts({ category, page, sort });
const setPage = (nextPage: number) =>
navigate({ search: (prev) => ({ ...prev, page: nextPage }) });
const setSort = (nextSort: "asc" | "desc") =>
navigate({ search: (prev) => ({ ...prev, sort: nextSort, page: 1 }) });
return (
<div>
<div className="flex gap-2 mb-4">
<button onClick={() => setSort("asc")}>A–Z</button>
<button onClick={() => setSort("desc")}>Z–A</button>
</div>
<ul>
{products?.map((product) => (
<li key={product.id}>
{/* TypeScript error if productId param is missing */}
<Link
to="/products/$productId"
params={ { productId: String(product.id) } }
activeProps={ { className: "font-bold text-sky-700" } }
>
{product.name}
</Link>
</li>
))}
</ul>
<button disabled={page <= 1} onClick={() => setPage(page - 1)}>Zurück</button>
<button onClick={() => setPage(page + 1)}>Weiter</button>
</div>
);
}
8. Code Splitting und lazy loading
Code Splitting in TanStack Router erfolgt über die lazyRouteComponent()-Funktion oder durch das Trennen von Loader und Komponente in separate Dateien. Die Route-Datei enthält dann nur die Metadaten (Loader, Search Param Schema, Params), während die Komponente über einen dynamischen Import geladen wird. Das bedeutet: Der initiale Bundle enthält nur den Router-Typ und die Loader. Die eigentliche UI-Komponente wird erst geladen, wenn die Route tatsächlich besucht wird.
Das File-based Routing Plugin unterstützt automatisches Code Splitting: Wenn eine Route-Datei einen component-Export als Default hat, wird dieser automatisch lazy geladen. Die pendingComponent-Option pro Route zeigt während des Ladens einen Skeleton oder Loading-Indikator. Im Gegensatz zu globalem React Suspense ist das pro Route konfigurierbar – kritische Routen können eager geladen werden, während sekundäre Bereiche der Applikation lazy bleiben. Das ergibt messbar bessere Time-to-Interactive-Werte ohne manuelle Bundle-Analyse.
9. TanStack Router vs. React Router v6 im Vergleich
Der Vergleich zeigt, wo TanStack Router gegenüber React Router v6 strukturelle Vorteile hat und wo die Grenzen liegen.
| Feature | React Router v6 | TanStack Router | Unterschied |
|---|---|---|---|
| Route Params | string | undefined | Vollständig typisiert | Kein manueller Guard nötig |
| Search Params | URLSearchParams, untypisiert | Schema + Typen + Defaults | Kein Parsing-Boilerplate |
| Navigation | String-basiert | Typsichere to/params/search | TypeScript-Fehler bei falschem Link |
| Loader | Nur mit Remix/Framework | Eingebaut, parallel | Kein Waterfall ohne Framework |
| Ökosystem | Groß, viele Ressourcen | Wächst, noch kleiner | React Router reifer |
React Router v6 bleibt die sichere Wahl für Teams mit großem Bestandscode und vielen Ressourcen im Internet. TanStack Router ist die richtige Wahl für neue TypeScript-Projekte, bei denen Typsicherheit in Navigation und URL-State ein ernstes Architekturziel ist. Die Migration von React Router zu TanStack Router ist aufwendig; bei neuen Projekten zahlt sich der Start mit TanStack Router von Anfang an aus.
Mironsoft
React Architektur, TanStack Router und typsichere Frontend-Systeme
Routing, das TypeScript wirklich kennt?
Wir implementieren TanStack Router in neuen React-Projekten und migrieren bestehende React-Router-Codebasen – mit typsicheren Params, Search Params, Loadern und Code Splitting.
Routing-Audit
Bestandsaufnahme der aktuellen Routing-Architektur und Identifikation von Typsicherheits-Lücken
Migration
Schrittweise Migration von React Router zu TanStack Router ohne Betriebsunterbrechung
Neuaufbau
TanStack Router von Grund auf mit File-based Routing, Loaders und TanStack Query Integration
10. Zusammenfassung
TanStack Router löst das fundamentale Typsicherheitsproblem von Routing in TypeScript-React-Applikationen. Routes als typisierte Objekte mit bekannten Params und Search Params machen es unmöglich, einen Link zu einer falschen Route oder mit falschen Params zu erstellen – der TypeScript-Compiler verhindert es. Loader eliminieren Datenabruf-Waterfalls ohne Framework. Search Params mit Schema und Standardwerten ersetzen manuelles URLSearchParams-Parsing. File-based Routing mit dem Vite-Plugin reduziert Routenkonfiguration auf das Minimum.
Die wichtigste Designentscheidung für Teams: TanStack Router ist ideal für neue TypeScript-Projekte, bei denen Typsicherheit ein Kernziel ist, und für Teams, die bereits TanStack Query einsetzen und von der nahtlosen Loader-Integration profitieren wollen. Die Lernkurve ist höher als bei React Router v6, zahlt sich aber durch weniger Laufzeitfehler und bessere Entwicklererfahrung schnell aus. Das Routing wird von einer häufigen Fehlerquelle zu einem statisch verifizierten Teil der Architektur.
TanStack Router — Das Wichtigste auf einen Blick
Typsichere Params
parseParams mit Zod-Schema transformiert rohe URL-Strings in typisierte Werte. useParams() gibt garantierte Typen zurück – kein undefined.
Search Params
Schema mit Standardwerten macht Search Params zu reaktivem, typsicherem URL-State. Funktionales Update-Muster verhindert unbeabsichtigtes Überschreiben.
Loader
Laufen parallel zu Elternrouten vor dem Render. Mit TanStack Query: ensureQueryData() im Loader, useSuspenseQuery() in der Komponente.
Navigation
Link to/params/search vollständig typisiert. TypeScript-Fehler bei falschem Routenpfad oder fehlendem Param – kein Laufzeitproblem mehr.