tanstack-router

安装量: 542
排名: #2016

安装

npx skills add https://github.com/jezweb/claude-skills --skill tanstack-router
TanStack Router
Type-safe, file-based routing for React SPAs with route-level data loading and TanStack Query integration
Quick Start
Last Updated
2026-01-09
Version
@tanstack/react-router@1.146.2 npm install @tanstack/react-router @tanstack/router-devtools npm install -D @tanstack/router-plugin

Optional: Zod validation adapter

npm
install
@tanstack/zod-adapter zod
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
├── 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
>
}
,
}
)
Virtual File Routes (v1.140+)
Programmatic route configuration when file-based conventions don't fit your needs:
Install
:
npm install @tanstack/virtual-file-routes
Vite Config
:
import
{
tanstackRouter
}
from
'@tanstack/router-plugin/vite'
export
default
defineConfig
(
{
plugins
:
[
tanstackRouter
(
{
target
:
'react'
,
virtualRouteConfig
:
'./routes.ts'
,
// Point to your routes file
}
)
,
react
(
)
,
]
,
}
)
routes.ts
(define routes programmatically):
import
{
rootRoute
,
route
,
index
,
layout
,
physical
}
from
'@tanstack/virtual-file-routes'
export
const
routes
=
rootRoute
(
'root.tsx'
,
[
index
(
'home.tsx'
)
,
route
(
'/posts'
,
'posts/posts.tsx'
,
[
index
(
'posts/posts-home.tsx'
)
,
route
(
'$postId'
,
'posts/posts-detail.tsx'
)
,
]
)
,
layout
(
'first'
,
'layout/first-layout.tsx'
,
[
route
(
'/nested'
,
'nested.tsx'
)
,
]
)
,
physical
(
'/classic'
,
'file-based-subtree'
)
,
// Mix with file-based
]
)
Use Cases
Custom route organization, mixing file-based and code-based, complex nested layouts.
Search Params Validation (Zod Adapter)
Type-safe URL search params with runtime validation:
Basic Pattern
(inline validation):
import
{
z
}
from
'zod'
export
const
Route
=
createFileRoute
(
'/products'
)
(
{
validateSearch
:
(
search
)
=>
z
.
object
(
{
page
:
z
.
number
(
)
.
catch
(
1
)
,
filter
:
z
.
string
(
)
.
catch
(
''
)
,
sort
:
z
.
enum
(
[
'newest'
,
'oldest'
,
'price'
]
)
.
catch
(
'newest'
)
,
}
)
.
parse
(
search
)
,
}
)
Recommended Pattern
(Zod adapter with fallbacks):
import
{
zodValidator
,
fallback
}
from
'@tanstack/zod-adapter'
import
{
z
}
from
'zod'
const
searchSchema
=
z
.
object
(
{
query
:
z
.
string
(
)
.
min
(
1
)
.
max
(
100
)
,
page
:
fallback
(
z
.
number
(
)
.
int
(
)
.
positive
(
)
,
1
)
,
sortBy
:
z
.
enum
(
[
'name'
,
'date'
,
'relevance'
]
)
.
optional
(
)
,
}
)
export
const
Route
=
createFileRoute
(
'/search'
)
(
{
validateSearch
:
zodValidator
(
searchSchema
)
,
// Type-safe: Route.useSearch() returns typed params
}
)
Why
.catch()
over
.default()
Use
.catch()
to silently fix malformed params. Use
.default()
+
errorComponent
to show validation errors.
Error Boundaries
Handle errors at route level with typed error components:
Route-Level Error Handling
:
export
const
Route
=
createFileRoute
(
'/posts/$postId'
)
(
{
loader
:
async
(
{
params
}
)
=>
{
const
post
=
await
fetchPost
(
params
.
postId
)
if
(
!
post
)
throw
new
Error
(
'Post not found'
)
return
{
post
}
}
,
errorComponent
:
(
{
error
,
reset
}
)
=>
(
<
div
>
<
p
>
Error
:
{
error
.
message
}
<
/
p
>
<
button onClick
=
{
reset
}
>
Retry
<
/
button
>
<
/
div
>
)
,
}
)
Default Error Component
(global fallback):
const
router
=
createRouter
(
{
routeTree
,
defaultErrorComponent
:
(
{
error
}
)
=>
(
<
div className
=
"error-page"
>
<
h1
>
Something went wrong
<
/
h1
>
<
p
>
{
error
.
message
}
<
/
p
>
<
/
div
>
)
,
}
)
Not Found Handling
:
export
const
Route
=
createFileRoute
(
'/posts/$postId'
)
(
{
notFoundComponent
:
(
)
=>
<
div
>
Post not found
<
/
div
>
,
}
)
Authentication with beforeLoad
Protect routes before they load (no flash of protected content):
Single Route Protection
:
import
{
redirect
}
from
'@tanstack/react-router'
export
const
Route
=
createFileRoute
(
'/dashboard'
)
(
{
beforeLoad
:
async
(
{
context
}
)
=>
{
if
(
!
context
.
auth
.
isAuthenticated
)
{
throw
redirect
(
{
to
:
'/login'
,
search
:
{
redirect
:
location
.
pathname
}
,
// Save for post-login
}
)
}
}
,
}
)
Protect Multiple Routes
(layout route pattern):
// routes/(authenticated)/route.tsx - protects all children
export
const
Route
=
createFileRoute
(
'/(authenticated)'
)
(
{
beforeLoad
:
async
(
{
context
}
)
=>
{
if
(
!
context
.
auth
.
isAuthenticated
)
{
throw
redirect
(
{
to
:
'/login'
}
)
}
}
,
}
)
Passing Auth Context
(from React hooks):
// main.tsx - pass auth state to router
function
App
(
)
{
const
auth
=
useAuth
(
)
// Your auth hook
return
(
<
RouterProvider
router
=
{
router
}
context
=
{
{
auth
}
}
// Available in beforeLoad
/
>
)
}
Known Issues Prevention
This skill prevents
20
documented issues:
Issue #1: Devtools Dependency Resolution
Error
Build fails with
@tanstack/router-devtools-core
not found
Fix
:
npm install @tanstack/router-devtools
Issue #2: Vite Plugin Order
(CRITICAL)
Error
Routes not auto-generated,
routeTree.gen.ts
missing
Fix
TanStackRouterVite MUST come before react() in plugins array
Why
Plugin processes route files before React compilation
Issue #3: Type Registration Missing
Error
:
not typed, no autocomplete
Fix
Import
routeTree
from
./routeTree.gen
in main.tsx to register types
Issue #4: Loader Not Running
Error
Loader function not called on navigation
Fix
Ensure route exports
Route
constant:
export const Route = createFileRoute('/path')({ loader: ... })
Issue #5: Memory Leak with TanStack Form
(FIXED)
Error
Production crashes when using TanStack Form + Router
Source
GitHub Issue #5734 (closed Jan 5, 2026)
Resolution
Fixed in latest versions of @tanstack/form and @tanstack/react-start. Update both packages to resolve.
Issue #6: Virtual Routes Index/Layout Conflict
Error
route.tsx and index.tsx conflict when using
physical()
in virtual routing
Source
GitHub Issue #5421
Fix
Use pathless route instead:
_layout.tsx
+
_layout.index.tsx
Issue #7: Search Params Type Inference
Error
Type inference not working with
zodSearchValidator
Source
GitHub Issue #3100 (regression since v1.81.5)
Fix
Use
zodValidator
from
@tanstack/zod-adapter
instead
Issue #8: TanStack Start Validators on Reload
Error
:
validateSearch
not working on page reload in TanStack Start
Source
GitHub Issue #3711
Note
Works on client-side navigation, fails on direct page load
Issue #9: Server Function Validation Errors Lose Structure
Error
:
inputValidator
Zod errors stringified, losing structure on client
Source
:
GitHub Issue #6428
Why It Happens
TanStack Start server function error serialization converts Zod issues array to JSON string in
error.message
, making it unusable without manual parsing.
Prevention
:
// Server function with input validation
export
const
myFn
=
createServerFn
(
{
method
:
'POST'
}
)
.
inputValidator
(
z
.
object
(
{
name
:
z
.
string
(
)
.
min
(
2
)
,
age
:
z
.
number
(
)
.
min
(
18
)
,
}
)
)
.
handler
(
async
(
{
data
}
)
=>
data
)
// Client: Workaround to parse stringified issues
try
{
await
mutation
.
mutate
(
{
data
:
invalidData
}
)
}
catch
(
error
)
{
if
(
error
.
message
.
startsWith
(
'['
)
)
{
const
issues
=
JSON
.
parse
(
error
.
message
)
// Now can use structured error data
issues
.
forEach
(
issue
=>
{
console
.
log
(
issue
.
path
,
issue
.
message
)
}
)
}
}
Official Status
Known issue, tracking PR for fix
Issue #10: useParams({ strict: false }) Returns Unparsed Values
Error
Params typed as parsed but returned as strings after navigation
Source
:
GitHub Issue #6385
Why It Happens
In v1.147.3+,
match.params
is no longer parsed when using
strict: false
. First render works correctly, but after navigation values are stored as strings instead of parsed types.
Prevention
:
// Route with param parsing
export
const
Route
=
createFileRoute
(
'/posts/$postId'
)
(
{
params
:
{
parse
:
(
params
)
=>
(
{
postId
:
z
.
coerce
.
number
(
)
.
parse
(
params
.
postId
)
,
}
)
,
}
,
}
)
// Component: Use strict mode (default) for parsed params
function
Component
(
)
{
const
{
postId
}
=
useParams
(
)
// ✓ Parsed as number
// const { postId } = useParams({ strict: false }) // ✗ String!
// Or manually parse when using strict: false
const
params
=
useParams
(
{
strict
:
false
}
)
const
postId
=
Number
(
params
.
postId
)
}
Official Status
Known issue, workaround required
Issue #11: Pathless Route notFoundComponent Not Rendering
Error
:
notFoundComponent
on pathless layout routes ignored
Source
:
GitHub Issue #6351
,
GitHub Issue #4065
Why It Happens
Pathless routes (e.g.,
routes/(authenticated)/route.tsx
) don't render their
notFoundComponent
. Instead, the
defaultNotFoundComponent
from router config is triggered. This has been broken since April 2025.
Prevention
:
// ✗ Doesn't work: notFoundComponent on pathless layout
export
const
Route
=
createFileRoute
(
'/(authenticated)'
)
(
{
beforeLoad
:
(
{
context
}
)
=>
{
if
(
!
context
.
auth
)
throw
redirect
(
{
to
:
'/login'
}
)
}
,
notFoundComponent
:
(
)
=>
<
div
>
Protected
404
<
/
div
>
,
// Not rendered!
}
)
// ✓ Works: Define on child routes instead
export
const
Route
=
createFileRoute
(
'/(authenticated)/dashboard'
)
(
{
notFoundComponent
:
(
)
=>
<
div
>
Protected
404
<
/
div
>
,
}
)
Official Status
Known issue, workaround required
Issue #12: Aborted Loader Renders errorComponent with Undefined Error
Error
Rapid navigation aborts previous loader and renders errorComponent with
undefined
error
Source
:
GitHub Issue #6388
Why It Happens
Side effect introduced after PR #4570. When user rapidly navigates (e.g., clicking through list items), aborted fetch requests trigger errorComponent without passing the abort error.
Prevention
:
export
const
Route
=
createFileRoute
(
'/posts/$postId'
)
(
{
loader
:
async
(
{
params
,
abortController
}
)
=>
{
await
fetch
(
`
/api/posts/
${
params
.
postId
}
`
,
{
signal
:
abortController
.
signal
,
}
)
}
,
errorComponent
:
(
{
error
,
reset
}
)
=>
{
// Check for undefined error (aborted request)
if
(
!
error
)
{
return
null
// Or show loading state
}
return
<
div
>
Error
:
{
error
.
message
}
<
/
div
>
}
,
}
)
Official Status
Known issue, workaround required
Issue #13: Vitest Cannot Read Properties of Null (useState)
Error
:
Cannot read properties of null (reading 'useState')
when running tests with Vitest
Source
:
GitHub Issue #6262
,
PR #6074
Why It Happens
TanStack Start's
tanstackStart()
plugin conflicts with Vitest's React hooks rendering. This is a known duplicate issue with a PR in progress.
Prevention
:
// Temporary workaround: Comment out tanstackStart() for tests
// vite.config.ts
export
default
defineConfig
(
{
plugins
:
[
// tanstackStart(), // Disable for tests
react
(
)
,
]
,
test
:
{
environment
:
'jsdom'
}
,
}
)
Official Status
PR #6074 in progress to fix
Issue #14: Throwing Error in Streaming SSR Loader Crashes Dev Server
Error
Dev server crashes when route loader throws error without awaiting (using
void
instead of
await
)
Source
:
GitHub Issue #6200
Why It Happens
SSR streaming mode can't handle unawaited promise rejections. The error escapes the loader context and crashes the worker process.
Prevention
:
// ✗ Wrong: void + throw crashes dev server
export
const
Route
=
createFileRoute
(
'/posts'
)
(
{
loader
:
async
(
)
=>
{
void
fetch
(
'/api/posts'
)
.
then
(
r
=>
{
throw
new
Error
(
'boom'
)
// Crashes!
}
)
}
,
}
)
// ✓ Correct: Always await or catch
export
const
Route
=
createFileRoute
(
'/posts'
)
(
{
loader
:
async
(
)
=>
{
try
{
const
data
=
await
fetch
(
'/api/posts'
)
return
data
}
catch
(
error
)
{
throw
error
// Caught by errorComponent
}
}
,
}
)
Official Status
Known issue, workaround required
Issue #15: Prerender Hangs Indefinitely if Filter Returns Zero Results
Error
Build step hangs when
prerender.filter
returns zero routes
Source
:
GitHub Issue #6425
Why It Happens
TanStack Start prerendering doesn't handle empty route sets gracefully - it waits indefinitely for routes that never come.
Prevention
:
// ✗ Wrong: Empty filter causes hang
tanstackStart
(
{
prerender
:
{
enabled
:
true
,
filter
:
(
route
)
=>
false
,
// No routes → hangs!
}
,
}
)
// ✓ Correct: Ensure at least one route or disable
tanstackStart
(
{
prerender
:
{
enabled
:
true
,
filter
:
(
route
)
=>
route
.
path
===
'/'
||
route
.
path
.
startsWith
(
'/posts'
)
,
}
,
}
)
// Or temporarily disable
tanstackStart
(
{
prerender
:
{
enabled
:
false
}
,
}
)
Official Status
Known issue, workaround required
Issue #16: Prerendering Does Not Work in Docker
Error
Build fails in Docker with "Unable to connect" during prerender step
Source
:
GitHub Issue #6275
,
PR #6305
Why It Happens
Vite preview server used for prerendering is not accessible in Docker environment.
Prevention
:
// vite.config.ts - Make preview server accessible in Docker
export
default
defineConfig
(
{
preview
:
{
host
:
true
,
// Bind to 0.0.0.0 instead of localhost
}
,
plugins
:
[
devtools
(
)
,
// nitro({ preset: "bun" }), // Remove temporarily if issues persist
tanstackStart
(
)
,
react
(
)
,
]
,
}
)
Official Status
PR #6305 in progress
Issue #17: Route Head Function Executes Before Loader Finishes
Error
Meta tags generated with incomplete data when
head()
runs before
loader()
Source
:
GitHub Issue #6221
Why It Happens
The
head()
function can execute before the route
loader()
finishes, causing meta tags to use placeholder or undefined data.
Prevention
:
// ✗ Wrong: loaderData may not be available yet
export
const
Route
=
createFileRoute
(
'/posts/$postId'
)
(
{
loader
:
async
(
{
params
}
)
=>
{
const
post
=
await
fetchPost
(
params
.
postId
)
return
{
post
}
}
,
head
:
(
{
loaderData
}
)
=>
(
{
meta
:
[
{
title
:
loaderData
.
post
.
title
}
,
// May be undefined!
]
,
}
)
,
}
)
// ✓ Correct: Explicitly await if needed
export
const
Route
=
createFileRoute
(
'/posts/$postId'
)
(
{
loader
:
async
(
{
params
}
)
=>
{
const
post
=
await
fetchPost
(
params
.
postId
)
return
{
post
}
}
,
head
:
async
(
{
loaderData
}
)
=>
{
await
loaderData
// Ensure loaded
return
{
meta
:
[
{
title
:
loaderData
.
post
.
title
}
]
,
}
}
,
}
)
Official Status
Known issue, workaround required
Issue #18: Virtual Routes Don't Support Manual Lazy Loading (Community-sourced)
Error
:
createLazyFileRoute
automatically replaced with
createFileRoute
in virtual routes
Source
:
GitHub Issue #6396
Why It Happens
Virtual file routes are designed for automatic code splitting only. Manual lazy routes are not supported - the plugin silently replaces them.
Prevention
:
// Virtual routes: Use automatic code splitting
// vite.config.ts
tanstackRouter
(
{
target
:
'react'
,
virtualRouteConfig
:
'./routes.ts'
,
autoCodeSplitting
:
true
,
// Use automatic splitting
}
)
// Don't use createLazyFileRoute in virtual routes
// It will be replaced with createFileRoute automatically
Official Status
By design (documented behavior)
Issue #19: NavigateOptions Type Safety Inconsistency (Community-sourced)
Error
:
NavigateOptions
type doesn't enforce required params like
useNavigate()
does
Source
:
TkDodo's Blog: The Beauty of TanStack Router
Why It Happens
Type definitions differ between runtime hook and type helper.
NavigateOptions
is less strict.
Prevention
:
// ✗ Wrong: NavigateOptions doesn't catch missing params
const
options
:
NavigateOptions
=
{
to
:
'/posts/$postId'
,
// No TS error, but params required!
}
// ✓ Correct: Use useNavigate() return type
const
navigate
=
useNavigate
(
)
type
NavigateFn
=
typeof
navigate
// Now type-safe across all usages
Verified
Cross-referenced with TanStack Query maintainer analysis
Issue #20: Missing Leading Slash in Route Paths (Community-sourced)
Error
Routes fail to match when path defined without leading slash
Source
:
Official Debugging Guide
Why It Happens
Very common beginner mistake - using
'about'
instead of
'/about'
causes route matching failures.
Prevention
:
// ✗ Wrong: Missing leading slash
export
const
Route
=
createFileRoute
(
'about'
)
(
{
/ ... /
}
)
// ✓ Correct: Always start with /
export
const
Route
=
createFileRoute
(
'/about'
)
(
{
/ ... /
}
)
Verified
Official documentation, common debugging issue 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 ( ) ) , } )
返回排行榜