tanstack-router by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill tanstack-router适用于 React SPA 的类型安全、基于文件的路由,具备路由级数据加载和 TanStack Query 集成功能
最后更新:2026-01-09 版本:@tanstack/react-router@1.146.2
npm install @tanstack/react-router @tanstack/router-devtools
npm install -D @tanstack/router-plugin
# 可选:Zod 验证适配器
npm install @tanstack/zod-adapter zod
Vite 配置 (TanStackRouterVite 必须在 react() 之前):
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [TanStackRouterVite(), react()], // 顺序很重要!
})
文件结构:
src/routes/
├── __root.tsx → 包含 <Outlet /> 的 createRootRoute()
├── index.tsx → createFileRoute('/')
└── posts.$postId.tsx → createFileRoute('/posts/$postId')
应用设置:
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // 由插件自动生成
const router = createRouter({ routeTree })
<RouterProvider router={router} />
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
类型安全导航 (路由自动补全,参数类型化):
<Link to="/posts/$postId" params={{ postId: '123' }} />
<Link to="/invalid" /> // ❌ TypeScript 错误
路由加载器 (渲染前获取数据):
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => ({ post: await fetchPost(params.postId) }),
component: ({ useLoaderData }) => {
const { post } = useLoaderData() // 完全类型化!
return <h1>{post.title}</h1>
},
})
TanStack Query 集成 (预取 + 缓存):
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>
},
})
当基于文件的约定不符合需求时,使用编程式路由配置:
安装:npm install @tanstack/virtual-file-routes
Vite 配置:
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
target: 'react',
virtualRouteConfig: './routes.ts', // 指向你的路由文件
}),
react(),
],
})
routes.ts (编程式定义路由):
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'), // 与基于文件的路由混合使用
])
使用场景:自定义路由组织、混合基于文件和基于代码的路由、复杂的嵌套布局。
具备运行时验证的类型安全 URL 搜索参数:
基本模式 (内联验证):
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),
})
推荐模式 (带回退的 Zod 适配器):
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),
// 类型安全:Route.useSearch() 返回类型化的参数
})
为什么使用 .catch() 而不是 .default():使用 .catch() 静默修复格式错误的参数。使用 .default() + errorComponent 来显示验证错误。
使用类型化的错误组件在路由级别处理错误:
路由级错误处理:
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>
),
})
默认错误组件 (全局回退):
const router = createRouter({
routeTree,
defaultErrorComponent: ({ error }) => (
<div className="error-page">
<h1>Something went wrong</h1>
<p>{error.message}</p>
</div>
),
})
未找到处理:
export const Route = createFileRoute('/posts/$postId')({
notFoundComponent: () => <div>Post not found</div>,
})
在路由加载前进行保护 (避免受保护内容闪现):
单路由保护:
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 }, // 保存用于登录后重定向
})
}
},
})
保护多个路由 (布局路由模式):
// routes/(authenticated)/route.tsx - 保护所有子路由
export const Route = createFileRoute('/(authenticated)')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
})
传递身份验证上下文 (从 React hooks):
// main.tsx - 将身份验证状态传递给路由器
function App() {
const auth = useAuth() // 你的身份验证钩子
return (
<RouterProvider
router={router}
context={{ auth }} // 在 beforeLoad 中可用
/>
)
}
此技能可预防 20 个已记录的问题:
@tanstack/router-devtools-core 未找到npm install @tanstack/router-devtools问题 #2:Vite 插件顺序 (关键)
routeTree.gen.ts 缺失问题 #3:类型注册缺失
<Link to="..."> 未类型化,无自动补全./routeTree.gen 导入 routeTree 以注册类型问题 #4:加载器未运行
Route 常量:export const Route = createFileRoute('/path')({ loader: ... })问题 #5:TanStack Form 内存泄漏 (已修复)
问题 #6:虚拟路由索引/布局冲突
physical() 时,route.tsx 和 index.tsx 冲突_layout.tsx + _layout.index.tsx问题 #7:搜索参数类型推断
zodSearchValidator 时类型推断不工作@tanstack/zod-adapter 中的 zodValidator问题 #8:TanStack Start 验证器在重新加载时失效
validateSearch 不工作错误:inputValidator Zod 错误被字符串化,在客户端丢失结构 来源:GitHub Issue #6428 发生原因:TanStack Start 服务器函数错误序列化将 Zod issues 数组转换为 error.message 中的 JSON 字符串,使其无法在没有手动解析的情况下使用。
预防:
// 带输入验证的服务器函数
export const myFn = createServerFn({ method: 'POST' })
.inputValidator(z.object({
name: z.string().min(2),
age: z.number().min(18),
}))
.handler(async ({ data }) => data)
// 客户端:解析字符串化问题的变通方案
try {
await mutation.mutate({ data: invalidData })
} catch (error) {
if (error.message.startsWith('[')) {
const issues = JSON.parse(error.message)
// 现在可以使用结构化的错误数据
issues.forEach(issue => {
console.log(issue.path, issue.message)
})
}
}
官方状态:已知问题,正在跟踪修复的 PR
错误:参数类型化为已解析但在导航后作为字符串返回 来源:GitHub Issue #6385 发生原因:在 v1.147.3+ 中,使用 strict: false 时,match.params 不再被解析。首次渲染工作正常,但导航后值存储为字符串而不是解析后的类型。
预防:
// 带参数解析的路由
export const Route = createFileRoute('/posts/$postId')({
params: {
parse: (params) => ({
postId: z.coerce.number().parse(params.postId),
}),
},
})
// 组件:使用严格模式(默认)获取解析后的参数
function Component() {
const { postId } = useParams() // ✓ 解析为数字
// const { postId } = useParams({ strict: false }) // ✗ 字符串!
// 或者在使用 strict: false 时手动解析
const params = useParams({ strict: false })
const postId = Number(params.postId)
}
官方状态:已知问题,需要变通方案
错误:无路径布局路由上的 notFoundComponent 被忽略 来源:GitHub Issue #6351, GitHub Issue #4065 发生原因:无路径路由(例如 routes/(authenticated)/route.tsx)不会渲染其 notFoundComponent。相反,会触发路由器配置中的 defaultNotFoundComponent。自 2025 年 4 月起此问题一直存在。
预防:
// ✗ 无效:无路径布局上的 notFoundComponent
export const Route = createFileRoute('/(authenticated)')({
beforeLoad: ({ context }) => {
if (!context.auth) throw redirect({ to: '/login' })
},
notFoundComponent: () => <div>Protected 404</div>, // 未渲染!
})
// ✓ 有效:在子路由上定义
export const Route = createFileRoute('/(authenticated)/dashboard')({
notFoundComponent: () => <div>Protected 404</div>,
})
官方状态:已知问题,需要变通方案
错误:快速导航中止先前的加载器并使用 undefined 错误渲染 errorComponent 来源:GitHub Issue #6388 发生原因:PR #4570 后引入的副作用。当用户快速导航(例如点击列表项)时,中止的 fetch 请求会触发 errorComponent 而不传递中止错误。
预防:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, abortController }) => {
await fetch(`/api/posts/${params.postId}`, {
signal: abortController.signal,
})
},
errorComponent: ({ error, reset }) => {
// 检查未定义的错误(中止的请求)
if (!error) {
return null // 或显示加载状态
}
return <div>Error: {error.message}</div>
},
})
官方状态:已知问题,需要变通方案
错误:使用 Vitest 运行测试时出现 Cannot read properties of null (reading 'useState') 来源:GitHub Issue #6262, PR #6074 发生原因:TanStack Start 的 tanstackStart() 插件与 Vitest 的 React hooks 渲染冲突。这是一个已知的重复问题,有 PR 正在进行中。
预防:
// 临时变通方案:为测试注释掉 tanstackStart()
// vite.config.ts
export default defineConfig({
plugins: [
// tanstackStart(), // 为测试禁用
react(),
],
test: { environment: 'jsdom' },
})
官方状态:PR #6074 正在进行中以修复
错误:当路由加载器抛出错误而未等待时(使用 void 而不是 await)开发服务器崩溃 来源:GitHub Issue #6200 发生原因:SSR 流模式无法处理未等待的 Promise 拒绝。错误逃逸了加载器上下文并导致工作进程崩溃。
预防:
// ✗ 错误:void + throw 导致开发服务器崩溃
export const Route = createFileRoute('/posts')({
loader: async () => {
void fetch('/api/posts').then(r => {
throw new Error('boom') // 崩溃!
})
},
})
// ✓ 正确:始终 await 或 catch
export const Route = createFileRoute('/posts')({
loader: async () => {
try {
const data = await fetch('/api/posts')
return data
} catch (error) {
throw error // 由 errorComponent 捕获
}
},
})
官方状态:已知问题,需要变通方案
错误:当 prerender.filter 返回零路由时构建步骤挂起 来源:GitHub Issue #6425 发生原因:TanStack Start 预渲染无法优雅处理空路由集 - 它会无限期等待永远不会到来的路由。
预防:
// ✗ 错误:空过滤器导致挂起
tanstackStart({
prerender: {
enabled: true,
filter: (route) => false, // 无路由 → 挂起!
},
})
// ✓ 正确:确保至少有一条路由或禁用
tanstackStart({
prerender: {
enabled: true,
filter: (route) => route.path === '/' || route.path.startsWith('/posts'),
},
})
// 或暂时禁用
tanstackStart({
prerender: { enabled: false },
})
官方状态:已知问题,需要变通方案
错误:在 Docker 中构建失败,预渲染步骤出现"无法连接" 来源:GitHub Issue #6275, PR #6305 发生原因:用于预渲染的 Vite 预览服务器在 Docker 环境中无法访问。
预防:
// vite.config.ts - 使预览服务器在 Docker 中可访问
export default defineConfig({
preview: {
host: true, // 绑定到 0.0.0.0 而不是 localhost
},
plugins: [
devtools(),
// nitro({ preset: "bun" }), // 如果问题持续存在,暂时移除
tanstackStart(),
react(),
],
})
官方状态:PR #6305 正在进行中
错误:当 head() 在 loader() 完成前运行时,使用不完整数据生成元标签 来源:GitHub Issue #6221 发生原因:head() 函数可以在路由 loader() 完成前执行,导致元标签使用占位符或未定义的数据。
预防:
// ✗ 错误:loaderData 可能尚不可用
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.post.title }, // 可能未定义!
],
}),
})
// ✓ 正确:如果需要则显式等待
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: async ({ loaderData }) => {
await loaderData // 确保已加载
return {
meta: [{ title: loaderData.post.title }],
}
},
})
官方状态:已知问题,需要变通方案
错误:虚拟路由中的 createLazyFileRoute 自动替换为 createFileRoute 来源:GitHub Issue #6396 发生原因:虚拟文件路由仅设计用于自动代码分割。不支持手动懒路由 - 插件会静默替换它们。
预防:
// 虚拟路由:使用自动代码分割
// vite.config.ts
tanstackRouter({
target: 'react',
virtualRouteConfig: './routes.ts',
autoCodeSplitting: true, // 使用自动分割
})
// 不要在虚拟路由中使用 createLazyFileRoute
// 它会自动替换为 createFileRoute
官方状态:设计如此 (已记录的行为)
错误:NavigateOptions 类型不像 useNavigate() 那样强制执行必需参数 来源:TkDodo's Blog: The Beauty of TanStack Router 发生原因:运行时钩子和类型辅助之间的类型定义不同。NavigateOptions 不够严格。
预防:
// ✗ 错误:NavigateOptions 不会捕获缺失的参数
const options: NavigateOptions = {
to: '/posts/$postId', // 无 TS 错误,但需要参数!
}
// ✓ 正确:使用 useNavigate() 返回类型
const navigate = useNavigate()
type NavigateFn = typeof navigate
// 现在在所有用法中都是类型安全的
已验证:与 TanStack Query 维护者分析交叉验证
错误:当路径定义没有前导斜杠时路由匹配失败 来源:官方调试指南 发生原因:非常常见的初学者错误 - 使用 'about' 而不是 '/about' 导致路由匹配失败。
预防:
// ✗ 错误:缺少前导斜杠
export const Route = createFileRoute('about')({ /* ... */ })
// ✓ 正确:始终以 / 开头
export const Route = createFileRoute('/about')({ /* ... */ })
已验证:官方文档,常见调试问题
Vite 配置 (添加 @cloudflare/vite-plugin):
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [TanStackRouterVite(), react(), cloudflare()],
})
API 路由模式 (从 Workers 后端获取):
// 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()),
})
相关技能:tanstack-query (数据获取), react-hook-form-zod (表单验证), cloudflare-worker-base (API 后端), tailwind-v4-shadcn (UI)
相关包:@tanstack/zod-adapter (搜索验证), @tanstack/virtual-file-routes (编程式路由)
最后验证:2026-01-20 | 技能版本:2.0.0 | 变更:从社区研究添加了 12 个新问题 (inputValidator 结构丢失, useParams 解析错误, 无路径 notFoundComponent, 中止的加载器错误, Vitest 冲突, SSR 流式崩溃, Docker 预渲染问题, head/loader 时序, 虚拟路由懒加载限制, NavigateOptions 类型不一致, 前导斜杠常见错误)。将错误预防从 8 个增加到 20 个已记录的问题。
每周安装
535
仓库
GitHub 星标
652
首次出现
Jan 31, 2026
安全审计
安装于
opencode414
codex378
gemini-cli371
claude-code363
github-copilot336
cursor255
Type-safe, file-based routing for React SPAs with route-level data loading and TanStack Query integration
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 <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} />
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>
},
})
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.
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.
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>,
})
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
/>
)
}
This skill prevents 20 documented issues:
@tanstack/router-devtools-core not foundnpm install @tanstack/router-devtoolsIssue #2: Vite Plugin Order (CRITICAL)
routeTree.gen.ts missingIssue #3: Type Registration Missing
<Link to="..."> not typed, no autocompleterouteTree from ./routeTree.gen in main.tsx to register typesIssue #4: Loader Not Running
Route constant: export const Route = createFileRoute('/path')({ loader: ... })Issue #5: Memory Leak with TanStack Form (FIXED)
Issue #6: Virtual Routes Index/Layout Conflict
physical() in virtual routing_layout.tsx + _layout.index.tsxIssue #7: Search Params Type Inference
zodSearchValidatorzodValidator from @tanstack/zod-adapter insteadIssue #8: TanStack Start Validators on Reload
validateSearch not working on page reload in TanStack StartError : 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
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
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
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
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
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
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
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
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
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)
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
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
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), react-hook-form-zod (form validation), cloudflare-worker-base (API backend), tailwind-v4-shadcn (UI)
Related Packages : @tanstack/zod-adapter (search validation), @tanstack/virtual-file-routes (programmatic routes)
Last verified : 2026-01-20 | Skill version : 2.0.0 | Changes : Added 12 new issues from community research (inputValidator structure loss, useParams parsing bug, pathless notFoundComponent, aborted loader errors, Vitest conflicts, SSR streaming crashes, Docker prerender issues, head/loader timing, virtual routes lazy loading limitation, NavigateOptions type inconsistency, leading slash common mistake). Increased error prevention from 8 to 20 documented issues.
Weekly Installs
535
Repository
GitHub Stars
652
First Seen
Jan 31, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode414
codex378
gemini-cli371
claude-code363
github-copilot336
cursor255
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装