TanStack Router vs React Router: Der entscheidende Vergleich
Lange war React Router die unangefochtene Wahl für React-Routing. TanStack Router hat das 2024 grundlegend verändert — und Claude Code nutzt es standardmäßig für alle neuen React-Projekte, die Type-Safety ernst nehmen.
VergleichWarum TanStack Router React Router ablöst
| Feature | TanStack Router | React Router v6 |
| Route-Parameter typsicher | ✓ 100% inferiert | ✗ manuell casten |
| Search Params typsicher | ✓ Zod-Schema | ✗ string only |
| Loader-Typen propagieren | ✓ automatisch | ~ teilweise |
| File-Based Routing | ✓ Vite Plugin | ✗ manuell |
| Devtools eingebaut | ✓ Browser Panel | ✗ nicht vorhanden |
| Code Splitting automatisch | ✓ per Route | ~ lazy() nötig |
| Context pro Route | ✓ routerContext | ✗ manuell |
| Pending / Error UI | ✓ eingebaut | ~ Suspense nötig |
Claude Code Empfehlung: Für jedes neue React-Projekt ohne Server-Rendering → TanStack Router. Für Next.js/Remix bleiben deren integrierten Router die bessere Wahl. Claude erkennt den Projektkontext und wählt automatisch die passende Lösung.
Type SafetyWas "vollständig typisiert" wirklich bedeutet
// Prompt: "Zeig mir warum TanStack Router typsicher ist vs React Router"
// ❌ React Router — keine Typen, Runtime-Fehler möglich
const { userId, orgId } = useParams()
// userId ist string | undefined — TypeScript hilft nicht
// Tippfehler "userID" → undefined, kein Compile-Fehler
// ✅ TanStack Router — vollständig inferiert
const { userId, orgId } = Route.useParams()
// userId ist EXAKT string — niemals undefined
// Tippfehler "userID" → TypeScript-Fehler sofort
// navigate({ to: '/users/$userId', params: { userId: 42 } }) → Fehler (number statt string)
// Search Params — vollständig validiert mit Zod
const { page, filter, sortBy } = Route.useSearch()
// page ist number (nicht string!), filter ist 'active'|'archived', sortBy hat Default
// URL: /users?page=2&filter=active → TypeScript weiss genau was rauskommt
File-Based Routing Setup mit dem Vite Plugin
Der schnellste Weg zu einem vollständig konfigurierten TanStack-Router-Projekt — Claude Code erledigt Installation, Konfiguration und erste Routen in einem Prompt.
SetupProjekt erstellen und konfigurieren
# Prompt: "Neues Vite+React+TypeScript Projekt mit TanStack Router File-Based Routing"
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @tanstack/react-router @tanstack/router-devtools
npm install -D @tanstack/router-plugin
// vite.config.ts — Vite Plugin einbinden
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite(), // ← Routes automatisch generieren
react(),
],
})
// src/routes/__root.tsx — Root Layout Route
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<nav>
<Link to="/">Home</Link>{' | '}
<Link to="/users">Users</Link>
</nav>
<Outlet /> {/* ← Kind-Routen rendern hier */}
<TanStackRouterDevtools />
</>
),
})
// src/routes/index.tsx — Home Route (/)
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: () => <div>Home Page</div>,
})
// src/routes/users/index.tsx — Users Liste (/users)
export const Route = createFileRoute('/users/')({
component: () => <div>Users List</div>,
})
// src/routes/users/$userId.tsx — User Detail (/users/42)
export const Route = createFileRoute('/users/$userId')({
component: () => {
const { userId } = Route.useParams() // ← string, typsicher
return <div>User {userId}</div>
},
})
Auto-Generierung: Das Vite Plugin beobachtet das routes/-Verzeichnis und generiert automatisch routeTree.gen.ts — alle Route-Typen werden daraus abgeleitet. Niemals diese Datei manuell editieren.
SetupDateistruktur und Naming Conventions
src/routes/
__root.tsx → Root Layout (/)
index.tsx → Home Page (/)
about.tsx → /about
users/
index.tsx → /users
$userId.tsx → /users/:userId (dynamisch)
$userId/
edit.tsx → /users/:userId/edit
posts.tsx → /users/:userId/posts
_auth/ → Underscore = Pathless Layout (kein URL-Segment)
dashboard.tsx → /dashboard (mit Auth-Guard aus _auth)
settings.tsx → /settings (mit Auth-Guard aus _auth)
(admin)/ → Klammern = Grouped Routes (kein URL-Segment)
users.tsx → /users (admin-spezifisch)
files/
$.tsx → /files/* (Catch-All)
Wichtig — Dateinamen sind URL-Segmente: $userId.tsx → Parameter userId. _layout.tsx → Pathless Layout (kein eigenes URL-Segment). (group) → Route Grouping ohne URL-Auswirkung. Claude Code kennt alle Konventionen und benennt Dateien korrekt.
Type-Safe Navigation mit useNavigate und Link
Navigation in TanStack Router ist vollständig typisiert — falsche Routen, fehlende Parameter oder falsche Typen werden bereits beim Schreiben als TypeScript-Fehler markiert, nicht erst zur Laufzeit.
Type SafeuseNavigate — Programmatische Navigation
// Prompt: "Typsichere Navigation nach User-Detail mit optionalen Query-Params"
import { useNavigate } from '@tanstack/react-router'
function UserActions({ userId }: { userId: string }) {
const navigate = useNavigate()
// ✅ Korrekt — TypeScript prüft alles
const goToUser = () => navigate({
to: '/users/$userId',
params: { userId }, // ← Pflichtfeld, muss string sein
search: { tab: 'profile' }, // ← optional, validiert gegen Schema
})
// ✅ Relative Navigation
const goToEdit = () => navigate({
from: '/users/$userId',
to: './edit', // ← relativ zur aktuellen Route
params: { userId },
})
// ✅ Replace statt Push (kein Zurück-Button Eintrag)
const redirectAfterLogin = () => navigate({
to: '/dashboard',
replace: true,
})
// ❌ Compile-Fehler — Route existiert nicht
// navigate({ to: '/user/$userId' }) → TypeScript meldet Fehler
// ❌ Compile-Fehler — params fehlen
// navigate({ to: '/users/$userId' }) → "params required"
return <button onClick={goToUser}>Zum Profil</button>
}
Type SafeLink-Komponente mit Active-State
// Prompt: "Navigation mit aktiven Link-States und preload on hover"
import { Link } from '@tanstack/react-router'
function NavBar() {
return (
<nav>
{/* activeProps werden bei aktiver Route angewendet */}
<Link
to="/"
activeProps={{ className: 'nav-active' }}
activeOptions={{ exact: true }} {/* Nur exakter Match */}
>
Home
</Link>
{/* Mit Parametern — vollständig typisiert */}
<Link
to="/users/$userId"
params={{ userId: '42' }}
search={{ tab: 'posts' }}
activeProps={{ style: { fontWeight: 'bold' } }}
>
User Profil
</Link>
{/* Preload on hover — Route lädt Daten bevor geklickt */}
<Link to="/dashboard" preload="intent">
Dashboard
</Link>
{/* Disabled State */}
<Link to="/admin" disabled={!isAdmin}>Admin</Link>
</nav>
)
}
// useMatchRoute — prüfen ob Route aktiv ist (ohne zu navigieren)
import { useMatchRoute } from '@tanstack/react-router'
function ConditionalUI() {
const matchRoute = useMatchRoute()
const isUserPage = matchRoute({ to: '/users/$userId' })
return isUserPage ? <UserSidebar /> : null
}
preload="intent": TanStack Router lädt Route-Daten (Loaders) automatisch wenn der Nutzer mit der Maus über einen Link fährt — noch bevor er klickt. Das Ergebnis: gefühlte Sofort-Navigation bei komplexen Datenquellen.
Search Params: Type-Safe URL-State mit Zod
Search Params sind einer der häufigsten Pain Points in React-Apps — meistens als rohes useSearchParams() ohne Typ-Sicherheit implementiert. TanStack Router löst das mit Zod-Validierung direkt in der Route-Definition.
Type SafevalidateSearch mit Zod-Schema
// Prompt: "User-Liste mit Pagination, Filter und Sortierung als typsichere URL-Parameter"
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { zodSearchValidator } from '@tanstack/router-zod-adapter'
const usersSearchSchema = z.object({
page: z.number().int().min(1).default(1),
limit: z.number().int().min(10).max(100).default(20),
filter: z.enum(['all', 'active', 'archived']).default('all'),
sortBy: z.enum(['name', 'email', 'createdAt']).default('name'),
sortOrder: z.enum(['asc', 'desc']).default('asc'),
search: z.string().optional(),
})
export const Route = createFileRoute('/users/')({
validateSearch: zodSearchValidator(usersSearchSchema),
component: UsersPage,
})
function UsersPage() {
const { page, limit, filter, sortBy, sortOrder, search } = Route.useSearch()
// Alle Typen sind korrekt: page ist number, filter ist 'all'|'active'|'archived'
// Defaults wurden bereits angewendet — niemals undefined
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (newPage: number) => navigate({
search: (prev) => ({ ...prev, page: newPage })
// ← Partial Update: nur page ändert sich, Rest bleibt
})
const setFilter = (f: 'all' | 'active' | 'archived') => navigate({
search: (prev) => ({ ...prev, filter: f, page: 1 })
// ← Beim Filtern: Seite zurücksetzen
})
return (
<div>
<FilterBar filter={filter} onFilterChange={setFilter} />
<UserTable users={users} sortBy={sortBy} sortOrder={sortOrder} />
<Pagination page={page} total={total} onPageChange={setPage} />
</div>
)
}
URL als Single Source of Truth: Nutzer können Seiten mit exaktem Filterzustand bookmarken, Links teilen und mit dem Browser-Back-Button navigieren — ohne dass React-State verloren geht. Claude Code implementiert dieses Pattern in unter 2 Minuten.
Type SafeSearch Params zwischen Routen weitergeben
// Prompt: "Search Params aus Parent-Route in Child-Route verfügbar machen"
// Parent: /search?q=claude&category=tools
export const Route = createFileRoute('/search')({
validateSearch: zodSearchValidator(z.object({
q: z.string().default(''),
category: z.string().optional(),
})),
})
// Child: /search/results — kann auf Parent Search Params zugreifen
export const Route = createFileRoute('/search/results')({
component: () => {
// useSearch mit from = Parent-Route
const { q, category } = useSearch({ from: '/search' })
return <Results query={q} category={category} />
},
})
Loaders und beforeLoad: Datenladen vor dem Render
TanStack Router hat Loaders und Guards als First-Class-Feature — ähnlich wie Remix, aber mit vollständiger TypeScript-Integration und automatischem Caching.
LoaderDaten laden bevor die Komponente rendert
// Prompt: "User-Detail Route mit Loader — Daten laden, Fehlerbehandlung, Loading-UI"
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
// Loader läuft VOR dem Render — Komponente bekommt garantiert Daten
loader: async ({ params, context }) => {
const user = await context.queryClient.ensureQueryData({
queryKey: ['users', params.userId],
queryFn: () => fetchUser(params.userId),
})
// Throw redirect für nicht-existierende User
if (!user) throw redirect({ to: '/users', search: { error: 'not-found' } })
return { user }
},
// Pending Component während Loader läuft
pendingComponent: () => <UserDetailSkeleton />,
// Error Component bei Loader-Fehler
errorComponent: ({ error }) => (
<div className="error">Fehler: {error.message}</div>
),
component: () => {
// useLoaderData ist SYNCHRON — Daten sind garantiert da
const { user } = Route.useLoaderData()
// user-Typ wird automatisch aus dem Loader-Return inferiert
return <UserDetail user={user} />
},
})
beforeLoadAuth Guards und Route Protection
// Prompt: "Auth Guard für alle /dashboard/* Routen mit Redirect zu Login"
// 1. Router Context aufsetzen — Auth in Context injizieren
import { createRouter, createRootRouteWithContext } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
interface RouterContext {
queryClient: QueryClient
auth: { isAuthenticated: boolean; user: User | null }
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
})
// 2. Pathless Auth Layout — _auth.tsx (kein URL-Segment)
export const Route = createFileRoute('/_auth')({
beforeLoad: ({ context, location }) => {
// beforeLoad läuft BEVOR Loader und BEVOR Render
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: {
redirect: location.href, // ← Return URL merken
},
})
}
},
component: () => <Outlet />,
})
// 3. Dashboard nutzt Auth-Guard automatisch (als Kind-Route)
// src/routes/_auth/dashboard.tsx → /dashboard (Auth-geschützt)
export const Route = createFileRoute('/_auth/dashboard')({
loader: async ({ context }) => {
// context.auth.user ist hier garantiert nicht null (Guard hat geprüft)
return await loadDashboardData(context.auth.user!.id)
},
component: Dashboard,
})
beforeLoad vs loader: beforeLoad läuft zuerst und kann redirecten oder Context anreichern. loader läuft danach und lädt Daten. Für Auth Guards immer beforeLoad verwenden — der Loader wird dann nur für authentifizierte Nutzer ausgeführt.
LoaderTanStack Query + Router: Das perfekte Duo
// Prompt: "TanStack Router Loaders mit React Query kombinieren für Prefetching"
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
context: { queryClient }, // ← QueryClient in Router Context
defaultPreload: 'intent', // ← Preload bei hover
defaultPreloadStaleTime: 0, // ← Immer fresh beim Preload
})
// In der Route — Query im Loader prefetchen
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context, params }) =>
context.queryClient.ensureQueryData({
queryKey: ['posts', params.postId],
queryFn: () => fetchPost(params.postId),
staleTime: 1000 * 60 * 5, // 5 Min Cache
}),
component: () => {
const params = Route.useParams()
// useQuery greift auf Cache zurück — KEIN erneuter Fetch nötig
const { data: post } = useQuery({
queryKey: ['posts', params.postId],
queryFn: () => fetchPost(params.postId),
})
return <PostDetail post={post!} />
},
})
Nested Layouts und Code Splitting
Nested Layouts und automatisches Code Splitting sind zwei der leistungsstärksten Features von TanStack Router — und Claude Code implementiert beides ohne manuellen Aufwand.
LayoutNested Layouts mit Outlet
// Prompt: "Multi-Level Nested Layout: App → Dashboard → Settings Sidebar"
// src/routes/__root.tsx — Globales Layout (Header/Footer für alle Seiten)
export const Route = createRootRoute({
component: () => (
<AppShell>
<GlobalHeader />
<Outlet /> {/* Dashboard-Layout oder normale Seiten */}
<GlobalFooter />
</AppShell>
),
})
// src/routes/_auth/dashboard.tsx — Dashboard-Layout
export const Route = createFileRoute('/_auth/dashboard')({
component: () => (
<DashboardShell>
<DashboardSidebar />
<main>
<Outlet /> {/* Settings, Overview, etc. */}
</main>
</DashboardShell>
),
})
// src/routes/_auth/dashboard/settings.tsx — Settings in Dashboard
export const Route = createFileRoute('/_auth/dashboard/settings')({
component: () => (
<SettingsPage>
<SettingsSidebar />
<Outlet /> {/* Profile, Security, etc. */}
</SettingsPage>
),
})
// URL: /dashboard/settings/profile
// Rendert: AppShell → DashboardShell → SettingsPage → ProfileForm
// Jede Ebene erhält NUR ihren Teil — kein Re-Render der Eltern
Granulares Re-Rendering: Bei Navigation von /dashboard/settings/profile zu /dashboard/settings/security re-rendert NUR die innerste Komponente. AppShell und DashboardShell bleiben stabil — das bedeutet: keine Flicker, keine verlorenen Scroll-Positionen.
Code SplittingAutomatisches Lazy Loading per Route
// Prompt: "Code Splitting für alle Dashboard-Routen — nur was gebraucht wird laden"
// Option 1: .lazy.tsx Suffix — automatisches Splitting
// src/routes/dashboard/analytics.lazy.tsx
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/dashboard/analytics')({
component: AnalyticsDashboard,
// pendingComponent wird angezeigt während JS-Chunk lädt
pendingComponent: () => <AnalyticsSkeleton />,
})
// Option 2: Manuelles Code Splitting mit lazyRouteComponent
import { lazyRouteComponent } from '@tanstack/react-router'
export const Route = createFileRoute('/reports')({
// Loader lädt parallel zum JS-Chunk
loader: () => loadReportData(),
component: lazyRouteComponent(
() => import('./ReportsPage'),
'ReportsPage' // Named Export
),
})
// Option 3: Virtual Route Splitting (fortgeschritten)
// Trennt Route-Definitionen von Komponenten für minimale Main-Bundle-Größe
export const Route = createFileRoute('/heavy-feature')({
loader: heavyLoader, // Bleibt im Main Bundle
}).lazy(() => import('./heavy-feature.lazy').then(d => d.Route))
Pending UILoading States und Error Boundaries pro Route
// Prompt: "Granulare Loading-States: Skeleton während Loader läuft, Error Fallback"
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/users/')({
loader: fetchUsers,
// Zeigt Skeleton während Loader noch nicht fertig ist
pendingComponent: () => (
<div className="users-grid">
{[...Array(6)].map((_, i) => <UserCardSkeleton key={i} />)}
</div>
),
pendingMs: 200, // ← Skeleton erst nach 200ms zeigen (verhindert Flicker)
pendingMinMs: 500, // ← Skeleton mindestens 500ms zeigen (verhindert Flicker)
// Error Boundary pro Route — kein globaler Crash
errorComponent: ({ error, reset }) => (
<div className="error-card">
<h3>Fehler beim Laden</h3>
<p>{error.message}</p>
<button onClick={reset}>Nochmal versuchen</button>
</div>
),
component: () => {
const users = Route.useLoaderData()
return <UsersGrid users={users} />
},
})
// notFoundComponent — eigene 404 pro Route-Bereich
export const Route = createFileRoute('/blog')({
notFoundComponent: () => <BlogNotFound />,
component: () => <Outlet />,
})
pendingMs + pendingMinMs: Diese zwei Parameter verhindern "Flicker" — zeige den Skeleton erst nach 200ms (kurze Requests zeigen gar keinen Loader) und mindestens 500ms (verhindert den unangenehmen kurzen Aufblitz). Claude Code setzt diese Werte standardmäßig.
Migration von React Router zu TanStack Router
Claude Code kann bestehende React-Router-Projekte migrieren — mit einem strukturierten Prompt und schrittweisem Vorgehen ohne Breaking Changes.
MigrationSchritt-für-Schritt Migrationspfad
- Abhängigkeiten austauschen:
react-router-dom deinstallieren, @tanstack/react-router + Vite Plugin installieren
- Route-Dateien erstellen: Für jede bestehende Route eine Datei in
routes/ anlegen — Claude Code mappt alle <Route path="..."> automatisch
- useParams migrieren:
useParams() → Route.useParams() — Claude Code findet alle Stellen und fügt Typen hinzu
- useNavigate migrieren:
navigate('/users/42') → navigate({ to: '/users/$userId', params: { userId: '42' } })
- Search Params typisieren:
useSearchParams() → validateSearch mit Zod-Schema definieren
- Loader extrahieren: Data Fetching aus Komponenten in Route
loader verschieben — Suspense-Boundary entfällt
- Guards migrieren:
<PrivateRoute>-Wrapper → Pathless Layout mit beforeLoad
Claude Code Migration Prompt: "Migriere diese React Router v6 Konfiguration zu TanStack Router mit File-Based Routing. Erstelle alle Route-Dateien, füge Typen hinzu und behalte die gleiche URL-Struktur." — Claude analysiert die bestehende Routing-Config und erstellt alle Dateien mit korrekten Typen.
DevtoolsTanStack Router Devtools nutzen
// Prompt: "Devtools für Development einbinden, in Production ausblenden"
import { Suspense, lazy } from 'react'
import { createRootRoute, Outlet } from '@tanstack/react-router'
// Lazy Import — Devtools nur in Development bundeln
const TanStackRouterDevtools =
process.env.NODE_ENV === 'production'
? () => null
: lazy(() =>
import('@tanstack/router-devtools').then((res) => ({
default: res.TanStackRouterDevtools,
}))
)
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
<Suspense>
<TanStackRouterDevtools
position="bottom-right"
initialIsOpen={false}
/>
</Suspense>
</>
),
})
Was die Devtools zeigen: Alle aktiven Routes, Match-Hierarchie, Loader-Status, Cache-Zustand, Search Params live — unverzichtbar beim Debugging von komplexen Nested Layouts und Loader-Chains.
100% Type-Safe
Routen, Parameter, Search Params und Loader-Daten — vollständig TypeScript-inferiert ohne manuelle Typ-Annotationen.
File-Based Routing
Vite Plugin generiert automatisch den Route-Tree — keine manuelle Router-Konfiguration mehr.
Loader + beforeLoad
Daten laden und Auth Guards vor dem Render — keine Suspense-Boundary mehr, kein Loading-State in Komponenten.
Search Param Zod
URL-State mit Zod validieren — Defaults, Typen, Coercion direkt in der Route-Definition.
Code Splitting
.lazy.tsx Suffix für automatisches per-Route Code Splitting — kleineres Initial Bundle ohne Konfiguration.
Preload on Intent
Loader starten beim Hover — Navigation fühlt sich sofortig an, auch bei komplexen Datenbankabfragen.
Claude Code für dein React-Projekt nutzen
TanStack Router, Type-Safe APIs, Zod-Validierung — Claude Code kennt alle modernen Patterns und implementiert sie in Minuten statt Stunden. Teste 14 Tage kostenlos.
Jetzt kostenlos starten →
Fazit: TanStack Router ist der neue Standard
TanStack Router löst das größte Problem mit React-Routing: fehlendes Type-System. Jede Route, jeder Parameter, jeder Search Param ist vollständig von TypeScript verstanden — Refactoring ist sicher, Tippfehler werden sofort erkannt, IDE-Autocompletion funktioniert überall.
Claude Code generiert TanStack-Router-Setups mit File-Based Routing, Zod-validierten Search Params, Auth Guards via beforeLoad und automatischem Code Splitting — vollständig konfiguriert und typsicher, ohne manuellen Boilerplate. Für neue React-Projekte ohne Server-Rendering ist TanStack Router 2026 die erste Wahl.