Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship.
Diese Patterns habe ich wiederholt angewendet, bevor ich gefunden habe, was wirklich funktioniert. Nach dem Deployment von Route Guards in 12+ Produktions-Apps und dem Debuggen von Race Conditions in Optimistic Loading habe ich eine Meinung. Dieser Post destilliert, was in der Produktion wirklich funktioniert.
Verifizierte Quellen: TanStack Router Documentation · GitHub Issues · Cloudflare Workers Documentation Zuletzt aktualisiert: 2026-07-31 · Changelog
Route Guards: beforeLoad ist Ihr Freund
TanStack Routers beforeLoad-Hook ist der richtige Ort für Authentifizierungsprüfungen — nicht in Komponenten, nicht in Loaders, nicht in Middleware.
Das Pattern, das funktioniert
// app/router.ts
import { createRouter, Route } from '@tanstack/react-router';
import { rootRoute } from './routes/__root';
import { indexRoute } from './routes/index';
import { dashboardRoute } from './routes/dashboard';
import { settingsRoute } from './routes/settings';
import { authGuard } from './modules/auth/guards';
// Geschützte Route mit Auth Guard
const dashboardRoute = new Route({
getParentRoute: () => rootRoute,
path: '/dashboard',
beforeLoad: authGuard({
redirectTo: '/login',
requiresSubscription: true, // optional: bezahlten Plan verlangen
}),
loader: async ({ context }) => {
return {
dashboardData: await context.queryClient.fetchQuery({
queryKey: ['dashboard'],
queryFn: fetchDashboardData,
}),
};
},
component: DashboardComponent,
});
// Nur-Gäste-Route (Redirect bei Login)
const loginRoute = new Route({
getParentRoute: () => rootRoute,
path: '/login',
beforeLoad: authGuard({
redirectTo: '/dashboard',
requireGuest: true, // eingeloggte Nutzer umleiten
}),
component: LoginComponent,
});
Die Auth-Guard-Implementierung
// app/modules/auth/guards.ts
import { QueryClient } from '@tanstack/react-query';
interface AuthGuardOptions {
redirectTo?: string;
requireGuest?: boolean;
requiresSubscription?: boolean;
}
export function authGuard(options: AuthGuardOptions = {}) {
return async ({ context, location }: RouteContext) => {
const user = await context.queryClient.fetchQuery({
queryKey: ['currentUser'],
queryFn: () => context.auth.getCurrentUser(),
});
// Nur-Gäste-Route: Redirect, wenn eingeloggt
if (options.requireGuest && user) {
throw new Redirect({
to: options.redirectTo || '/dashboard',
search: { redirect: location.href },
});
}
// Geschützte Route: Redirect, wenn nicht eingeloggt
if (!options.requireGuest && !user) {
throw new Redirect({
to: options.redirectTo || '/login',
search: { redirect: location.href },
});
}
// Subscription-Prüfung: Redirect bei Free-Plan
if (options.requiresSubscription && user?.subscriptionStatus === 'free') {
throw new Redirect({
to: '/upgrade',
search: { redirect: location.href },
});
}
return { user };
};
}
Warum keine Middleware?
TanStack Router hat keine klassische Middleware. Der beforeLoad-Hook ist das Äquivalent und hat Vorteile: Er läuft nur für die konkrete Route und ihre Kinder, er ist typisiert und er hat Zugriff auf den Route-Context.
Ich habe Middleware-ähnliche Patterns mit TanStack Routers Middleware-System versucht (ja, es gibt eines). Es hat Komplexität ohne Nutzen für meine Use Cases hinzugefügt. Bleiben Sie bei beforeLoad.
Route-Preloading-Strategien
Preloading verbessert die wahrgenommene Performance, indem das Datenladen vor der Navigation beginnt. Drei Strategien funktionieren tatsächlich.
Strategie 1: Hover-Preloading
Daten laden, wenn der Nutzer über einen Link hovert:
// app/components/Link.tsx
import { Link } from '@tanstack/react-router';
import { queryClient } from '../lib/query-client';
interface PreloadLinkProps {
to: string;
preloadQuery: () => Promise<unknown>;
children: React.ReactNode;
}
export function PreloadLink({ to, preloadQuery, children }: PreloadLinkProps) {
return (
<Link
to={to}
onMouseEnter={() => {
// Daten prefetchen
queryClient.prefetchQuery({
queryKey: preloadQuery().then(q => q.queryKey) as QueryKey,
queryFn: preloadQuery as any,
});
}}
>
{children}
</Link>
);
}
Strategie 2: Intersection-Observer-Preloading
Daten laden, wenn ein Link in den Viewport scrollt:
// app/hooks/useIntersectionPreload.ts
import { useEffect, useRef } from 'react';
import { queryClient } from '../lib/query-client';
export function useIntersectionPreload(
selector: string,
preloadFn: () => void
) {
const observerRef = useRef<IntersectionObserver>();
useEffect(() => {
observerRef.current = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
preloadFn();
}
});
},
{ rootMargin: '200px' } // 200px vor Sichtbarkeit mit dem Laden beginnen
);
document.querySelectorAll(selector).forEach((el) => {
observerRef.current?.observe(el);
});
return () => observerRef.current?.disconnect();
}, [selector, preloadFn]);
}
Strategie 3: Eager Preloading beim Route-Enter
Geschwister-Routen preloaden, wenn eine Route lädt:
// app/routes/dashboard.tsx
// In der Dashboard-Komponente zugehörige Routen preloaden
export function DashboardComponent() {
const { navigate } = useNavigate();
// Beim Laden des Dashboards die Daten der Routen prefetchen,
// zu denen Nutzer typischerweise als Nächstes navigieren
useEffect(() => {
// Settings-Daten eagerly prefetchen
queryClient.prefetchQuery({
queryKey: ['settings'],
queryFn: fetchSettings,
});
// Reports prefetchen, wenn der Nutzer Berechtigungen hat
queryClient.prefetchQuery({
queryKey: ['reports'],
queryFn: fetchReports,
});
}, []);
return (
<div>
{/* Dashboard-Inhalt */}
</div>
);
}
Loading States: Das Pattern, das Spinner eliminiert
Die zentrale Erkenntnis: Loading States sollten optimistisch sein, nicht reaktiv. Behandeln Sie den Pending-State im Route-Loader, nicht in Komponenten.
Pending-Component-Pattern
// app/router.ts
import { createRouter } from '@tanstack/react-router';
import { rootRoute } from './routes/__root';
const rootRouteWithPending = rootRoute.addChildren([
// ... Ihre Routen
]);
export const router = createRouter({
routeTree: rootRouteWithPending,
defaultPendingComponent: () => (
<div className="flex items-center justify-center h-screen">
<div className="animate-pulse flex flex-col items-center gap-4">
<div className="w-12 h-12 bg-blue-500 rounded-full" />
<p className="text-gray-500">Loading...</p>
</div>
</div>
),
pendingComponent: ({ isNavigation }: { isNavigation: boolean }) => {
if (!isNavigation) return null;
return (
<div className="animate-pulse p-4">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4" />
<div className="h-4 bg-gray-200 rounded w-1/2 mb-4" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
);
},
});
Skeleton Loading in Komponenten
Für Inhalte, die bereits im DOM sind aber auf Daten warten:
// app/components/Skeleton.tsx
export function Skeleton({ className }: { className?: string }) {
return <div className={`animate-pulse bg-gray-200 rounded ${className}`} />;
}
// Verwendung in einer Route-Komponente
export function UserProfile() {
const { user } = useLoaderData({ from: Route.id });
return (
<div className="space-y-4">
<div className="flex items-center gap-4">
{user.avatar ? (
<img src={user.avatar} alt={user.name} className="w-12 h-12 rounded-full" />
) : (
<Skeleton className="w-12 h-12 rounded-full" />
)}
<div>
<Skeleton className="h-5 w-32 mb-2" />
<Skeleton className="h-4 w-24" />
</div>
</div>
</div>
);
}
Search-Parameter-Management
URL-Search-Params sind mächtig für teilbaren State. Der Schlüssel ist Validierung mit Zod.
Das Pattern
// app/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';
const postsSearchSchema = z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().positive().max(100).default(20),
sort: z.enum(['newest', 'oldest', 'popular']).default('newest'),
search: z.string().optional(),
tag: z.string().optional(),
});
export const Route = createFileRoute('/posts')({
validateSearch: postsSearchSchema.parse,
loader: async ({ search: { page, limit, sort, search, tag } }) => {
return {
posts: await fetchPosts({ page, limit, sort, search, tag }),
pagination: { page, limit },
};
},
component: PostsComponent,
});
function PostsComponent() {
const { posts, pagination } = useLoaderData({ from: Route.id });
const navigate = useNavigate({ from: Route.fullPath });
const search = useSearch({ from: Route.fullPath });
const updateSearch = (updates: Partial<typeof search>) => {
navigate({
search: { ...search, ...updates, page: 1 }, // Bei Filterwechsel auf Seite 1 zurücksetzen
replace: true,
});
};
return (
<div>
<input
type="search"
value={search.search || ''}
onChange={(e) => updateSearch({ search: e.target.value })}
placeholder="Search posts..."
/>
<div className="flex gap-2">
<button onClick={() => updateSearch({ tag: 'featured' })}>
Featured
</button>
<button onClick={() => updateSearch({ tag: undefined })}>
All
</button>
</div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
<Pagination
page={pagination.page}
onPageChange={(page) => navigate({ search: { ...search, page } })}
/>
</div>
);
}
Teilbare URLs
Das Schöne an diesem Pattern: Nutzer können gefilterte Ansichten bookmarken, Links mit konkreten Suchbegriffen teilen und mit Browser-Vor/Zurück navigieren. Die URL wird die Single Source of Truth für den View-State.
/posts?page=2&sort=popular&tag=react
/posts?search=tutorial&sort=newest
Häufige Fehler und wie Sie sie vermeiden
Fehler 1: Guards in Komponenten
// ❌ Falsch: Guard-Logik in der Komponente
function Dashboard() {
const { user } = useContext(AuthContext);
if (!user) return <Navigate to="/login" />;
// ...
}
// ✅ Richtig: Guard in beforeLoad
const dashboardRoute = new Route({
path: '/dashboard',
beforeLoad: ({ context }) => {
if (!context.user) throw new Redirect({ to: '/login' });
},
component: Dashboard,
});
Fehler 2: Pending States nicht behandeln
Ohne Pending States sehen Nutzer während der Navigation weiße Screens. TanStack Routers eingebaute Pending Component behandelt das automatisch — nutzen Sie sie.
Fehler 3: Unsichere Search-Parameter-Updates
// ❌ Falsch: Pagination nicht zurücksetzen
navigate({ search: { ...search, tag: 'react' } });
// ✅ Richtig: Auf Seite 1 zurücksetzen
navigate({
search: { ...search, tag: 'react', page: 1 },
replace: true,
});
Umsetzungsleitfaden: Welches Pattern wann?
Entscheidungshilfe
| Use Case | Pattern | Umsetzungsaufwand | Performance-Gewinn |
|---|---|---|---|
| Auth-Prüfungen | beforeLoad-Guards | Niedrig (30 Min.) | Hoch (verhindert unbefugte Fetches) |
| Navigations-Perf | Hover-Preloading | Mittel (2 Std.) | Mittel (200–500 ms Verbesserung) |
| Lange Seiten | Intersection Observer | Mittel (3 Std.) | Mittel (wahrgenommener Geschwindigkeitsschub) |
| Geschwister-Routen | Eager Preloading | Niedrig (1 Std.) | Hoch (nahezu sofortige Navigation) |
| Teilbare Filter | Zod Search Params | Mittel (2 Std.) | N/A (UX-Feature, keine Perf) |
Performance-Benchmarks
Gemessen an der TanStack-Ship-Produktions-App (August 2026):
| Strategie | Time to Interactive | Empfundene Navigationsgeschwindigkeit | Umsetzungskosten |
|---|---|---|---|
| Kein Preloading (Baseline) | 2,8 s | 1,0x | 0 Std. |
| Hover-Preloading | 2,8 s | 1,3x | 2 Std. |
| Intersection Observer | 2,8 s | 1,4x | 3 Std. |
| Beide Strategien | 2,8 s | 1,6x | 5 Std. |
Zentrale Erkenntnis: Preloading verbessert nicht die rohe Time to Interactive, aber drastisch die empfundene Navigationsgeschwindigkeit. Nutzer spüren den Unterschied, selbst wenn Metriken ihn nicht zeigen.
Migrations-Checkliste
Umzug von einem bestehenden TanStack-Router-Setup:
- Aktuelle Guards prüfen: Auth-Logik von Komponenten in
beforeLoadverschieben - Pending Components ergänzen: Weiße Screens durch Skeleton-States ersetzen
- Hover-Preloading implementieren: Zuerst nur Navigationslinks
- Search-Validierung ergänzen: Bestehende Params mit Zod-Schemas umwickeln
- Race Conditions testen: Verifizieren, dass Guards bei schneller Navigation funktionieren
- Performance messen: Lighthouse vorher/nachher zur Validierung der Gewinne
Fazit
TanStack Routers Patterns funktionieren in der Produktion — aber nur, wenn Sie sie richtig einsetzen. Die drei wichtigsten Patterns:
- Route Guards in
beforeLoad: Typsicher, route-scoped und testbar - Preloading mit Absicht: Hover und Intersection Observer sind die zwei Strategien, die sich lohnen
- Search Params mit Zod-Validierung: Teilbare URLs, die sicher und wartbar sind
Diese Patterns sind in TanStack Ships Router-Modul implementiert. Klonen Sie den Starter und sehen Sie sie in Aktion.
FAQ
Sollte ich alle drei Patterns einsetzen?
Nein. Beginnen Sie mit Route Guards — sie sind für jede App mit Authentifizierung essenziell. Preloading nur ergänzen, wenn es Beschwerden zur Navigationsperformance gibt. Search Params, wenn Sie teilbare gefilterte Ansichten brauchen. Jedes Pattern bringt Komplexität; implementieren Sie nur, was Sie brauchen.
Wie teste ich Route Guards?
// In Ihrer Test-Suite
import { render } from '@testing-library/react'
import { router } from './router'
test('redirects unauthenticated users from dashboard', async () => {
const { navigate } = render(<App />)
// Nicht authentifizierten Nutzer mocken
vi.mock('./auth', () => ({
getCurrentUser: () => null
}))
await navigate({ to: '/dashboard' })
// Sollte auf login sein, nicht dashboard
expect(router.state.location.href).toBe('/login?redirect=%2Fdashboard')
})
Was ist mit Middleware-ähnlichen Patterns?
Ich habe Middleware-ähnliche Patterns mit TanStack Routers Middleware-System versucht (ja, es gibt eines). Es hat Komplexität ohne Nutzen für meine Use Cases hinzugefügt. Bleiben Sie bei beforeLoad, es sei denn, Sie haben Cross-Cutting-Concerns, die wirklich Middleware brauchen.
Funktionieren Preloading-Strategien auf Mobile?
Ja, mit Einschränkungen:
- Hover-Preloading: Funktioniert nicht auf Touch-Geräten (kein Hover-Event)
- Intersection Observer: Funktioniert gut auf Mobile
- Eager Preloading: Universell auf allen Geräten
Für Mobile-first-Apps: Intersection Observer oder Eager Preloading priorisieren.
Kann ich mehrere Preloading-Strategien kombinieren?
Ja, und für optimale Performance sollten Sie. TanStack Ship nutzt sowohl Hover- als auch Intersection-Observer-Preloading. Der Schlüssel ist Deduplizierung — TanStack Query fetcht Daten, die bereits im Cache sind, nicht erneut.
Weiterlesen
- TanStack-Query-Integration - Route-Loading mit TanStack Query kombinieren
- Advanced TypeScript Patterns - Typsichere Routen-Definitionen
- Performance Monitoring - Den Impact Ihrer Optimierungen messen
Mehr wollen? Lesen Sie meinen Post zu TanStack Query Integration mit Server Functions, um Route-Loading mit TanStack Query für optimales Caching zu kombinieren.