TanStack Router: Typsicheres Routing in React
AI generated
</>
{ }
React · TanStack Router · TypeScript · Routing
TanStack Router:
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.

15 Min. Lesezeit Routes · Params · Search Params · Loaders · Code Splitting TanStack Router 1.x · React 18/19 · TypeScript 5

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>
  );
}

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.

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.

11. FAQ: TanStack Router und typsicheres Routing

1TanStack Router mit Next.js?
Nicht kompatibel. TanStack Router ist für Vite-SPAs konzipiert. Next.js nutzt den eigenen App Router. TanStack Query ist die sinnvolle Next.js-Ergänzung.
2Produktionsreif?
Ja. Version 1.0 seit Ende 2023 stabil. Typisierungsinfrastruktur ausgereift. Ökosystem wächst schnell.
3Integration mit TanStack Query?
QueryClient im Router-Kontext. Loader rufen ensureQueryData() auf. Komponente liest mit useSuspenseQuery(). Kein doppelter Fetch.
4Code Splitting vs. React.lazy()?
lazyRouteComponent() teilt Komponente und Loader auf Route-Ebene auf. Kleinerer initialer Bundle ohne manuelles Chunk-Management.
5Migration von React Router?
Kein paralleler Betrieb möglich. Schrittweise Route für Route oder Big-Bang für kleinere Codebasen. Loader und Typen müssen neu implementiert werden.
6SSR-Unterstützung?
TanStack Start (experimentell) basiert auf TanStack Router + Vinxi. Für produktionsreifes SSR aktuell Next.js oder Remix empfohlen.
7Route Guards implementieren?
beforeLoad in der Routendefinition prüft Auth. throw redirect({ to: '/login' }) leitet um. Kontext trägt Auth-State für alle Routen zugänglich.
8Layout Routes?
Rendern nur <Outlet />, teilen Layout-Elemente über Kindrouten. In File-based Routing: __layout.tsx-Dateien. Keine eigene URL.
9Parallele Outlet-Bereiche?
Named Outlets via id-Prop. Haupt-Outlet und Side-Panel-Outlet unabhängig navigierbar. Für komplexe Dashboard-Layouts.
10Search Params mit Zod validieren?
validateSearch: (raw) => schema.parse(raw). Bei jeder Navigation ausgeführt. Ungültige Werte auf Standardwerte zurücksetzen oder Fehler werfen.