TanStack Router provides fully type-safe routing — route params, search params, and loader data all carry TypeScript types inferred from the route definition. File-based routing generates the route tree automatically from a routes/ directory. createFileRoute("/orders/$orderId") defines a route with a typed $orderId param. loader fetches data server-side (or at navigation time) — useLoaderData() returns the fully-typed result. Search params are validated with Zod and typed — useSearch() returns the validated type. Nested layouts compose route segments. Route guards redirect unauthenticated users. RouterDevtools visualizes the route tree and pending states. Claude Code generates TanStack Router route files, loader functions, search param schemas, layout components, and the Vite configuration for file-based route generation.
CLAUDE.md for TanStack Router
## TanStack Router Stack
- Version: @tanstack/react-router >= 1.65, @tanstack/router-devtools >= 1.65
- File-based: use @tanstack/router-vite-plugin — generates routeTree.gen.ts automatically
- Routes: src/routes/ directory — _layout.tsx for layouts, index.tsx for root
- Params: createFileRoute("/orders/$orderId") — $orderId inferred in useParams
- Loaders: route.loader = async ({ params, context }) => {...} — prefetched at navigation
- Search: searchParams with validateSearch: zodValidator — typed search
- Guards: beforeLoad throws redirect(302, { to: "/login" }) for auth
- Data: useLoaderData() — fully typed return from loader function
Vite Configuration
// vite.config.ts — TanStack Router file-based routing
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import { TanStackRouterVite } from "@tanstack/router-vite-plugin"
export default defineConfig({
plugins: [
TanStackRouterVite(), // Auto-generates routeTree.gen.ts
react(),
],
})
Route Files
// src/routes/__root.tsx — root layout route
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import type { QueryClient } from "@tanstack/react-query"
interface RouterContext {
queryClient: QueryClient
auth: {
isAuthenticated: boolean
userId: string | null
}
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
})
function RootLayout() {
return (
<>
<nav>
<a href="/">Home</a>
<a href="/orders">Orders</a>
<a href="/account">Account</a>
</nav>
<main>
<Outlet />
</main>
{import.meta.env.DEV && <TanStackRouterDevtools />}
</>
)
}
// src/routes/_auth.tsx — authenticated layout wrapper
import { createFileRoute, redirect, Outlet } from "@tanstack/react-router"
export const Route = createFileRoute("/_auth")({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: "/login",
search: { redirect: location.href },
})
}
},
component: AuthLayout,
})
function AuthLayout() {
return (
<div className="authenticated-layout">
<Outlet />
</div>
)
}
// src/routes/_auth/orders/index.tsx — orders list route
import {
createFileRoute,
useLoaderData,
Link,
useSearch,
} from "@tanstack/react-router"
import { z } from "zod"
import { zodValidator } from "@tanstack/zod-adapter"
import { fetchOrders } from "@/api/orders"
// Typed search params with Zod validation
const ordersSearchSchema = z.object({
status: z.enum(["all", "pending", "shipped", "delivered"]).default("all"),
page: z.number().int().positive().default(1),
search: z.string().optional(),
})
export const Route = createFileRoute("/_auth/orders/")({
// Validates and types search params
validateSearch: zodValidator(ordersSearchSchema),
// Loader: fetches data before rendering, React Query-aware
loader: async ({ context, deps: { status, page, search } }) => {
const { userId } = context.auth
return context.queryClient.ensureQueryData({
queryKey: ["orders", userId, { status, page, search }],
queryFn: () => fetchOrders({ customerId: userId!, status, page, search }),
})
},
// Memoize loader based on search params
loaderDeps: ({ search: { status, page, search } }) => ({ status, page, search }),
component: OrdersPage,
})
function OrdersPage() {
// Fully typed — inferred from loader return type
const { orders, totalCount, pageCount } = useLoaderData({ strict: false })
// Typed search — validated by Zod schema
const { status, page, search } = useSearch({ strict: false })
const navigate = useNavigate()
return (
<div>
<div className="filters">
<select
value={status}
onChange={e => navigate({ search: s => ({ ...s, status: e.target.value as any }) })}
>
<option value="all">All</option>
<option value="pending">Pending</option>
<option value="shipped">Shipped</option>
</select>
<input
value={search ?? ""}
onChange={e => navigate({ search: s => ({ ...s, search: e.target.value || undefined }) })}
placeholder="Search orders..."
/>
</div>
<p>{totalCount} orders</p>
{orders.map(order => (
<Link key={order.id} to="/orders/$orderId" params={{ orderId: order.id }}>
<div>
<span>#{order.id.slice(-8)}</span>
<span>{order.status}</span>
<span>${(order.totalCents / 100).toFixed(2)}</span>
</div>
</Link>
))}
<div className="pagination">
{page > 1 && (
<button onClick={() => navigate({ search: s => ({ ...s, page: page - 1 }) })}>
Previous
</button>
)}
{page < pageCount && (
<button onClick={() => navigate({ search: s => ({ ...s, page: page + 1 }) })}>
Next
</button>
)}
</div>
</div>
)
}
// src/routes/_auth/orders/$orderId.tsx — dynamic order detail route
import { createFileRoute, useLoaderData, notFound } from "@tanstack/react-router"
import { fetchOrder } from "@/api/orders"
export const Route = createFileRoute("/_auth/orders/$orderId")({
loader: async ({ params: { orderId }, context }) => {
const order = await context.queryClient.ensureQueryData({
queryKey: ["orders", orderId],
queryFn: () => fetchOrder(orderId),
})
if (!order) throw notFound()
return order
},
// Custom not found component
notFoundComponent: () => <p>Order not found</p>,
component: OrderDetailPage,
})
function OrderDetailPage() {
const order = useLoaderData({ strict: false })
// order.id, order.status, etc. — all typed from fetchOrder return type
return (
<div>
<h1>Order #{order.id.slice(-8)}</h1>
<p>Status: {order.status}</p>
<p>Total: ${(order.totalCents / 100).toFixed(2)}</p>
<ul>
{order.items.map(item => (
<li key={item.productId}>
{item.name} × {item.quantity} = ${((item.priceCents * item.quantity) / 100).toFixed(2)}
</li>
))}
</ul>
</div>
)
}
Router Setup with Context
// src/main.tsx — router with context providers
import { createRouter, RouterProvider } from "@tanstack/react-router"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { routeTree } from "./routeTree.gen" // Auto-generated
import { useAuthStore } from "./stores/auth"
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 30_000 } },
})
const router = createRouter({
routeTree,
defaultPreload: "intent", // Preload on hover
defaultPreloadStaleTime: 0,
context: {
queryClient,
auth: { isAuthenticated: false, userId: null }, // Updated by provider
},
})
// Register router instance type for useRouter() type inference
declare module "@tanstack/react-router" {
interface Register { router: typeof router }
}
function App() {
const auth = useAuthStore()
return (
<QueryClientProvider client={queryClient}>
<RouterProvider
router={router}
context={{
queryClient,
auth: { isAuthenticated: !!auth.userId, userId: auth.userId },
}}
/>
</QueryClientProvider>
)
}
For the React Router v7 (formerly Remix) alternative that offers similar file-based routing with server actions and loader patterns with SSR support baked in, see the Remix guide for nested route patterns. For the Next.js App Router that provides a different file-based approach with React Server Components and server actions instead of client-side loaders, see the Next.js guide for RSC patterns. The Claude Skills 360 bundle includes TanStack Router skill sets covering file-based routes, loaders, and search params. Start with the free tier to try TanStack Router configuration generation.