TanStack Router
TanStack Router is a fully type-safe router for React that treats routes, loaders, params, and search params as first-class typed data rather than untyped strings.
Workflow for Adding a New Route
- Create the route file — Add a file under
src/routes/ following the file-based routing convention (see below).
- Define the route — Export
Route from createFileRoute('/path')({...}) with component, and optionally loader, validateSearch, beforeLoad, errorComponent, and pendingComponent.
- Validate search params — If the route reads query params, define a Zod schema and pass it as
validateSearch.
- Load data — Fetch data in
loader, not in a component useEffect; integrate with TanStack Query via queryClient.ensureQueryData when caching is needed.
- Guard access — Add
beforeLoad checks (e.g. auth) that throw redirect({ to: '/login' }) when a precondition fails.
- Link to the route — Navigate with
<Link to="/path" params={...} search={...}> so the compiler validates params and search at every call site.
- Regenerate the route tree — Ensure
routeTree.gen.ts is regenerated (automatic under the Vite plugin's dev server / build) before running or building the app.
Core Principles
- TanStack Router is 100% type-safe — lean on TypeScript generics for params, search params, and loader data instead of manual casting.
- Prefer file-based routing with
@tanstack/router-vite-plugin (or @tanstack/router-plugin/vite) for scalability over manually constructed route trees.
- Always define routes with
createFileRoute (leaf/nested routes) or createRootRoute / createRootRouteWithContext (root).
- Route data loading belongs in
loader functions, not in component useEffect — this enables preloading, parallel loading, and pending/error states.
- Search params are first-class state — always define their schema with Zod (or another standard-schema validator) so they are typed and validated on every read.
File-Based Route Conventions
src/routes/
__root.tsx ← Root layout
index.tsx ← / route
posts/
index.tsx ← /posts
$postId.tsx ← /posts/:postId (dynamic segment)
_layout.tsx ← Layout route (no path segment)
_auth/ ← Pathless auth layout group
dashboard.tsx
- A leading underscore on a segment (
_layout, _auth) creates a pathless layout route used purely for grouping/shared UI.
- A
$ prefix ($postId) marks a dynamic path segment, matching Route.useParams().
index.tsx inside a folder matches the folder's own path with no additional segment.
Route Definition
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => fetchPost(params.postId),
component: PostComponent,
errorComponent: ({ error }) => <ErrorBanner message={error.message} />,
pendingComponent: () => <PostSkeleton />,
})
function PostComponent() {
const post = Route.useLoaderData() // type-safe
const { postId } = Route.useParams() // type-safe
return <div>{post.title}</div>
}
errorComponent renders when the loader throws; pendingComponent renders while the loader is in flight (subject to defaultPendingMs).
- Loader return values and thrown errors are both fully typed and flow into
useLoaderData() and errorComponent respectively.
Type-Safe Search Params
- Always define search params with Zod and
validateSearch.
- Access them with
Route.useSearch() — never read window.location.search or URLSearchParams directly, which bypasses type safety and validation.
const searchSchema = z.object({
page: z.number().int().min(1).default(1),
q: z.string().optional(),
})
export const Route = createFileRoute('/search')({
validateSearch: searchSchema,
component: SearchPage,
})
function SearchPage() {
const { page, q } = Route.useSearch()
const navigate = Route.useNavigate()
return (
<button => navigate({ search: (prev) => ({ ...prev, page: page + 1 }) })}>
Next page
</button>
)
}
Navigation
- Use
<Link> for internal navigation — never a raw <a href>, which triggers a full page reload and loses client-side routing state.
- Always pass typed
params and search; the TypeScript compiler catches missing or mistyped route params at build time.
<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link>
<Link to="/search" search={{ page: 1, q: 'react' }}>Search</Link>
Loaders + TanStack Query Integration
- Put the
QueryClient in router context so loaders can prefetch and cache through TanStack Query rather than duplicating fetch logic.
- Use
ensureQueryData (not fetchQuery) in loaders so an already-cached query is reused instead of refetched.
export const Route = createFileRoute('/posts')({
loader: ({ context: { queryClient } }) =>
queryClient.ensureQueryData(postsQueryOptions()),
component: PostsPage,
})
function PostsPage() {
const { data: posts } = useSuspenseQuery(postsQueryOptions())
return <PostList posts={posts} />
}
Router Context for Dependency Injection
// __root.tsx
interface RouterContext {
queryClient: QueryClient
auth: AuthState
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
})
// main.tsx
const router = createRouter({ routeTree, context: { queryClient, auth } })
- Router context flows down to every route's
loader and beforeLoad via the context argument, giving each route typed access to shared dependencies without prop drilling or global singletons.
Auth Guards
export const Route = createFileRoute('/_auth/dashboard')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
},
component: Dashboard,
})
beforeLoad runs before the loader and before the component renders, making it the right place for auth checks, feature flag gates, and redirects.
- Prefer
throw redirect(...) over imperative navigation inside components — it works during SSR, preloading, and client navigation alike.
Performance
- Set
defaultPreload: 'intent' on the router so links prefetch their route's data on hover/focus, making navigation feel instant.
- Use
React.lazy (or the router's built-in code-splitting via .lazy() route files) for route component code splitting on large apps.
- Install
@tanstack/router-devtools and render <TanStackRouterDevtools /> in development to inspect route matches, pending states, and cache.
Common Mistakes
- Fetching data with
useEffect inside a route component instead of a loader, which loses preloading and creates render-then-fetch waterfalls.
- Reading raw
window.location.search instead of Route.useSearch(), bypassing the search param schema and type safety.
- Performing auth checks only inside components (
useEffect redirect) instead of beforeLoad, allowing a flash of protected content before redirecting.
- Forgetting to add new route params/search fields to
<Link> call sites — let the compiler catch it rather than testing manually.
1---2name: tanstack-router3description: Type-safe, file-based routing for React with TanStack Router. Use when defining routes with createFileRoute, validating search params with Zod, writing route loaders, setting up auth guards with beforeLoad, integrating TanStack Query into loaders, or configuring router context and preloading.4---5
6# TanStack Router
7
8TanStack Router is a fully type-safe router for React that treats routes, loaders, params, and search params as first-class typed data rather than untyped strings.
9
10## Workflow for Adding a New Route
11
121. **Create the route file** — Add a file under `src/routes/` following the file-based routing convention (see below).
132. **Define the route** — Export `Route` from `createFileRoute('/path')({...})` with `component`, and optionally `loader`, `validateSearch`, `beforeLoad`, `errorComponent`, and `pendingComponent`.
143. **Validate search params** — If the route reads query params, define a Zod schema and pass it as `validateSearch`.
154. **Load data** — Fetch data in `loader`, not in a component `useEffect`; integrate with TanStack Query via `queryClient.ensureQueryData` when caching is needed.
165. **Guard access** — Add `beforeLoad` checks (e.g. auth) that `throw redirect({ to: '/login' })` when a precondition fails.
176. **Link to the route** — Navigate with `<Link to="/path" params={...} search={...}>` so the compiler validates params and search at every call site.
187. **Regenerate the route tree** — Ensure `routeTree.gen.ts` is regenerated (automatic under the Vite plugin's dev server / build) before running or building the app.
19
20## Core Principles
21
22- TanStack Router is 100% type-safe — lean on TypeScript generics for params, search params, and loader data instead of manual casting.
23- Prefer file-based routing with `@tanstack/router-vite-plugin` (or `@tanstack/router-plugin/vite`) for scalability over manually constructed route trees.
24- Always define routes with `createFileRoute` (leaf/nested routes) or `createRootRoute` / `createRootRouteWithContext` (root).
25- Route data loading belongs in `loader` functions, not in component `useEffect` — this enables preloading, parallel loading, and pending/error states.
26- Search params are first-class state — always define their schema with Zod (or another standard-schema validator) so they are typed and validated on every read.
27
28## File-Based Route Conventions
29
30```
31src/routes/
32 __root.tsx ← Root layout
33 index.tsx ← / route
34 posts/
35 index.tsx ← /posts
36 $postId.tsx ← /posts/:postId (dynamic segment)
37 _layout.tsx ← Layout route (no path segment)
38 _auth/ ← Pathless auth layout group
39 dashboard.tsx
40```
41
42- A leading underscore on a segment (`_layout`, `_auth`) creates a pathless layout route used purely for grouping/shared UI.
43- A `$` prefix (`$postId`) marks a dynamic path segment, matching `Route.useParams()`.
44- `index.tsx` inside a folder matches the folder's own path with no additional segment.
45
46## Route Definition
47
48```tsx
49export const Route = createFileRoute('/posts/$postId')({
50 loader: async ({ params }) => fetchPost(params.postId),
51 component: PostComponent,
52 errorComponent: ({ error }) => <ErrorBanner message={error.message} />,
53 pendingComponent: () => <PostSkeleton />,
54})
55
56function PostComponent() {
57 const post = Route.useLoaderData() // type-safe
58 const { postId } = Route.useParams() // type-safe
59 return <div>{post.title}</div>
60}
61```
62
63- `errorComponent` renders when the loader throws; `pendingComponent` renders while the loader is in flight (subject to `defaultPendingMs`).
64- Loader return values and thrown errors are both fully typed and flow into `useLoaderData()` and `errorComponent` respectively.
65
66## Type-Safe Search Params
67
68- Always define search params with Zod and `validateSearch`.
69- Access them with `Route.useSearch()` — never read `window.location.search` or `URLSearchParams` directly, which bypasses type safety and validation.
70
71```tsx
72const searchSchema = z.object({
73 page: z.number().int().min(1).default(1),
74 q: z.string().optional(),
75})
76
77export const Route = createFileRoute('/search')({
78 validateSearch: searchSchema,
79 component: SearchPage,
80})
81
82function SearchPage() {
83 const { page, q } = Route.useSearch()
84 const navigate = Route.useNavigate()
85
86 return (
87 <button onClick={() => navigate({ search: (prev) => ({ ...prev, page: page + 1 }) })}>
88 Next page
89 </button>
90 )
91}
92```
93
94## Navigation
95
96- Use `<Link>` for internal navigation — never a raw `<a href>`, which triggers a full page reload and loses client-side routing state.
97- Always pass typed `params` and `search`; the TypeScript compiler catches missing or mistyped route params at build time.
98
99```tsx
100<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link>
101<Link to="/search" search={{ page: 1, q: 'react' }}>Search</Link>
102```
103
104## Loaders + TanStack Query Integration
105
106- Put the `QueryClient` in router context so loaders can prefetch and cache through TanStack Query rather than duplicating fetch logic.
107- Use `ensureQueryData` (not `fetchQuery`) in loaders so an already-cached query is reused instead of refetched.
108
109```tsx
110export const Route = createFileRoute('/posts')({
111 loader: ({ context: { queryClient } }) =>
112 queryClient.ensureQueryData(postsQueryOptions()),
113 component: PostsPage,
114})
115
116function PostsPage() {
117 const { data: posts } = useSuspenseQuery(postsQueryOptions())
118 return <PostList posts={posts} />
119}
120```
121
122## Router Context for Dependency Injection
123
124```tsx
125// __root.tsx
126interface RouterContext {
127 queryClient: QueryClient
128 auth: AuthState
129}
130
131export const Route = createRootRouteWithContext<RouterContext>()({
132 component: RootLayout,
133})
134
135// main.tsx
136const router = createRouter({ routeTree, context: { queryClient, auth } })
137```
138
139- Router context flows down to every route's `loader` and `beforeLoad` via the `context` argument, giving each route typed access to shared dependencies without prop drilling or global singletons.
140
141## Auth Guards
142
143```tsx
144export const Route = createFileRoute('/_auth/dashboard')({
145 beforeLoad: ({ context }) => {
146 if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
147 },
148 component: Dashboard,
149})
150```
151
152- `beforeLoad` runs before the loader and before the component renders, making it the right place for auth checks, feature flag gates, and redirects.
153- Prefer `throw redirect(...)` over imperative navigation inside components — it works during SSR, preloading, and client navigation alike.
154
155## Performance
156
157- Set `defaultPreload: 'intent'` on the router so links prefetch their route's data on hover/focus, making navigation feel instant.
158- Use `React.lazy` (or the router's built-in code-splitting via `.lazy()` route files) for route component code splitting on large apps.
159- Install `@tanstack/router-devtools` and render `<TanStackRouterDevtools />` in development to inspect route matches, pending states, and cache.
160
161## Common Mistakes
162
163- Fetching data with `useEffect` inside a route component instead of a `loader`, which loses preloading and creates render-then-fetch waterfalls.
164- Reading raw `window.location.search` instead of `Route.useSearch()`, bypassing the search param schema and type safety.
165- Performing auth checks only inside components (`useEffect` redirect) instead of `beforeLoad`, allowing a flash of protected content before redirecting.
166- Forgetting to add new route params/search fields to `<Link>` call sites — let the compiler catch it rather than testing manually.