| name | TanStack Router |
| description | Build type-safe, file-based React routing with TanStack Router. Supports client-side navigation, route loaders, and TanStack Query integration. Use when implementing file-based routing patterns, building SPAs with TypeScript routing, or troubleshooting devtools dependency errors, type safety issues, or Vite bundling problems. |
TanStack Router
Type-safe, file-based routing for React SPAs with route-level data loading and TanStack Query integration
Quick Start
Last Updated: 2025-11-28 Version: @tanstack/react-router@1.139.10
npm install @tanstack/react-router @tanstack/router-devtools
npm install -D @tanstack/router-plugin
Vite Config (TanStackRouterVite MUST come before react()):
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [TanStackRouterVite(), react()], // Order matters!
})
File Structure:
src/routes/
├── __root.tsx → createRootRoute() with <Outlet />
├── index.tsx → createFileRoute('/')
└── posts.$postId.tsx → createFileRoute('/posts/$postId')
App Setup:
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // Auto-generated by plugin
const router = createRouter({ routeTree })
<RouterProvider router={router} />
Core Patterns
Type-Safe Navigation (routes auto-complete, params typed):
<Link to="/posts/$postId" params={{ postId: '123' }} />
<Link to="/invalid" /> // ❌ TypeScript error
Route Loaders (data fetching before render):
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => ({ post: await fetchPost(params.postId) }),
component: ({ useLoaderData }) => {
const { post } = useLoaderData() // Fully typed!
return <h1>{post.title}</h1>
},
})
TanStack Query Integration (prefetch + cache):
const postOpts = (id: string) => queryOptions({
queryKey: ['posts', id],
queryFn: () => fetchPost(id),
})
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context: { queryClient }, params }) =>
queryClient.ensureQueryData(postOpts(params.postId)),
component: () => {
const { postId } = Route.useParams()
const { data } = useQuery(postOpts(postId))
return <h1>{data.title}</h1>
},
})
Known Issues & Solutions
Issue #1: Devtools Dependency Resolution
- Error: Build fails with
@tanstack/router-devtools-corenot found - Fix:
npm install @tanstack/router-devtools
Issue #2: Vite Plugin Order (CRITICAL)
- Error: Routes not auto-generated,
routeTree.gen.tsmissing - Fix: TanStackRouterVite MUST come before react() in plugins array
- Why: Plugin processes route files before React compilation
Issue #3: Type Registration Missing
- Error:
<Link to="...">not typed, no autocomplete - Fix: Import
routeTreefrom./routeTree.genin main.tsx to register types
Issue #4: Loader Not Running
- Error: Loader function not called on navigation
- Fix: Ensure route exports
Routeconstant:export const Route = createFileRoute('/path')({ loader: ... })
Issue #5: Memory Leak with TanStack Form
- Error: Production crashes when using TanStack Form + Router
- Source: GitHub Issue #5734 (known issue, unfixed as of v1.139)
- Workaround: Use React Hook Form or Formik instead
Cloudflare Workers Integration
Vite Config (add @cloudflare/vite-plugin):
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [TanStackRouterVite(), react(), cloudflare()],
})
API Routes Pattern (fetch from Workers backend):
// Worker: functions/api/posts.ts
export async function onRequestGet({ env }) {
const { results } = await env.DB.prepare('SELECT * FROM posts').all()
return Response.json(results)
}
// Router: src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async () => fetch('/api/posts').then(r => r.json()),
})
Related Skills: tanstack-query (data fetching), cloudflare-worker-base (API backend), tailwind-v4-shadcn (UI)