TanStack Router — Type-Safe Routing
Overview
TanStack Router is a fully type-safe client-side router for React (and Solid). Every route, param, search param, and loader data is typed end-to-end. It supports file-based routing, built-in data loading with caching, search param schemas, and nested layouts — without needing a meta-framework.
When to Use
- SPAs that need type-safe routing (React Router doesn't provide full type safety)
- Apps where search params carry complex structured state
- React apps wanting data loading with caching similar to Next.js without going full SSR
- Replacing React Router in a large TypeScript codebase
- Building a SPA but wanting Remix-like loader/action patterns
Installation / Setup
npm install @tanstack/react-router
# File-based routing (optional):
npm install -D @tanstack/router-plugin @tanstack/router-devtools
// vite.config.ts (file-based routing)
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [TanStackRouterVite(), react()],
});
Key Patterns
File-Based Route Convention
src/routes/
__root.tsx → Root layout
index.tsx → /
about.tsx → /about
posts/
index.tsx → /posts
$postId.tsx → /posts/:postId
_auth/ → Pathless layout (auth wrapper)
dashboard.tsx → /dashboard
Root Route
// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
export const Route = createRootRoute({
component: () => (
<>
<nav>
<Link to="/" activeProps={{ className: 'active' }}>Home</Link>
<Link to="/posts" activeProps={{ className: 'active' }}>Posts</Link>
</nav>
<Outlet />
<TanStackRouterDevtools />
</>
),
});
Route with Loader and Params
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// params.postId is typed as string
const post = await fetchPost(params.postId);
return { post };
},
component: function Post() {
const { post } = Route.useLoaderData(); // fully typed
const { postId } = Route.useParams(); // typed as { postId: string }
return <article><h1>{post.title}</h1></article>;
},
});
Type-Safe Search Params
// src/routes/users/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';
const searchSchema = z.object({
page: z.number().catch(1),
q: z.string().optional(),
sort: z.enum(['asc', 'desc']).catch('asc'),
});
export const Route = createFileRoute('/users/')({
validateSearch: searchSchema,
loader: async ({ location: { search } }) => {
return fetchUsers({ page: search.page, query: search.q, sort: search.sort });
},
component: function Users() {
const search = Route.useSearch(); // typed: { page: number; q?: string; sort: 'asc' | 'desc' }
const navigate = Route.useNavigate();
return (
<div>
<input
value={search.q ?? ''}
=> navigate({ search: { ...search, q: e.target.value, page: 1 } })}
/>
</div>
);
},
});
Code-Split Routes (Manual)
// src/routeTree.gen.ts is auto-generated by the plugin
// For manual setup:
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
const router = createRouter({ routeTree });
declare module '@tanstack/react-router' {
interface Register { router: typeof router; }
}
Authentication Guard
// src/routes/_auth.tsx (pathless layout)
import { createFileRoute, redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/_auth')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login', search: { redirect: location.href } });
}
},
component: () => <Outlet />,
});
Common Pitfalls
- Regenerate routeTree after adding files: the plugin auto-generates
routeTree.gen.ts— restart dev server if routes aren't found catch()in search schemas is required: withoutcatch, invalid URL params throw instead of defaultinguseSearchreturns the whole search, not just the route's schema: useRoute.useSearch()(notuseSearch()) for scoped type safety- Loader data is refetched on navigation: unlike SWR/React Query, loaders run on each navigation — add
staleTimeor integrate React Query for caching - Type inference requires TypeScript 5+: older TS versions may struggle with the deep inference patterns
Related Skills
tanstack-form— the form library from the same ecosystemreact-query-tanstack— data fetching that pairs with TanStack Router loaderszod-expert— search param validationreact-best-practices— React fundamentals
GitNexus Index
domain: frontend/web
tier: library
runtime: browser
language: tsx,ts
framework: react,solid
purpose: routing