关于 tanstack-router
TanStack Router 是面向 React SPA 的类型安全路由库,适用于需要文件路由和路由级数据加载的开发者。它支持 TanStack Query 集成、搜索参数验证和虚拟路由配置。
主要功能
- 01基于文件的类型安全路由
- 02路由级数据加载
- 03集成 TanStack Query 预取与缓存
- 04验证 URL 搜索参数
- 05路由级错误与未找到处理
- 06支持虚拟文件路由
适用场景
- 01为 React SPA 配置文件路由
- 02在页面渲染前加载路由数据
- 03校验并处理 URL 搜索参数
- 04组织复杂的嵌套路由和布局
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill tanstack-router安装后在 Claude Code 中输入/tanstack-router确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.md.claude-plugin/plugin.jsonreferences/common-errors.mdrules/tanstack-router.mdtemplates/package.jsonroute-examples.tsxvite.config.ts详细介绍
TanStack Router
为 React SPA 提供类型安全的基于文件的路由,支持路由级数据加载和 TanStack Query 集成
快速开始
最后更新:2026-01-09 版本:@tanstack/[email protected]
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 配置(TanStackRouterVite 必须放在 react() 之前):
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [TanStackRouterVite(), react()], // Order matters!
})
文件结构:
src/routes/
├── __root.tsx → createRootRoute() with <Outlet />
├── index.tsx → createFileRoute('/')
└── posts.$postId.tsx → createFileRoute('/posts/$postId')
应用设置:
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // Auto-generated by plugin
const router = createRouter({ routeTree })
<RouterProvider router={router} />
核心模式
类型安全的导航(路由自动补全,参数带类型):
<Link to="/posts/$postId" params={{ postId: '123' }} />
<Link to="/invalid" /> // ❌ TypeScript error
路由加载器(渲染前获取数据):
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 集成(预取 + 缓存):
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>
},
})
虚拟文件路由(v1.140+)
当基于文件的约定无法满足需求时,可通过编程方式配置路由:
安装:npm install @tanstack/virtual-file-routes
Vite 配置:
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(以编程方式定义路由):
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
])
使用场景:自定义路由组织方式、混合使用基于文件和基于代码的路由、复杂的嵌套路由布局。
搜索参数验证(Zod 适配器)
对 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),
// Type-safe: Route.useSearch() returns typed params
})
为什么使用 .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>,
})
使用 beforeLoad 进行身份验证
在路由加载前保护路由(不会短暂显示受保护的内容):
保护单个路由:
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
})
}
},
})
保护多个路由(布局路由模式):
// routes/(authenticated)/route.tsx - protects all children
export const Route = createFileRoute('/(authenticated)')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
})
传递身份验证上下文(来自 React 钩子):
// main.tsx - pass auth state to router
function App() {
const auth = useAuth() // Your auth hook
return (
<RouterProvider
router={router}
context={{ auth }} // Available in beforeLoad
/>
)
}
已知问题预防
此技能可预防已记录的 20 个问题:
问题 #1:Devtools 依赖解析
- 错误:构建失败,提示找不到
@tanstack/router-devtools-core - 修复:
npm install @tanstack/router-devtools
问题 #2:Vite 插件顺序(严重)
- 错误:路由未自动生成,缺少
routeTree.gen.ts - 修复:在 plugins 数组中,TanStackRouterVite 必须放在 react() 之前
- 原因:插件会在 React 编译前处理路由文件
问题 #3:缺少类型注册
- 错误:
<Link to="...">没有类型,也无法自动补全 - 修复:在 main.tsx 中从
./routeTree.gen导入routeTree,以注册类型
问题 #4:加载器未运行
- 错误:导航时未调用加载器函数
- 修复:确保路由导出
Route常量:export const Route = createFileRoute('/path')({ loader: ... })
问题 #5:使用 TanStack Form 时发生内存泄漏(已修复)
- 错误:同时使用 TanStack Form + Router 时,生产环境发生崩溃
- 来源:GitHub Issue #5734(2026 年 1 月 5 日已关闭)
- 解决方案:已在最新版 @tanstack/form 和 @tanstack/react-start 中修复。更新这两个包即可解决。
问题 #6:虚拟路由中的索引路由/布局冲突
- 错误:在虚拟路由中使用
physical()时,route.tsx 和 index.tsx 冲突 - 来源:GitHub Issue #5421
- 修复:改用无路径路由:
_layout.tsx+_layout.index.tsx
问题 #7:搜索参数类型推断
- 错误:
zodSearchValidator的类型推断无法正常工作 - 来源:GitHub Issue #3100(自 v1.81.5 起出现回归问题)
- 修复:改用来自
@tanstack/zod-adapter的zodValidator
问题 #8:TanStack Start 重载时的验证器
- 错误:在 TanStack Start 中,页面重载时
validateSearch无法正常工作 - 来源:GitHub Issue #3711
- 注意:客户端导航时正常工作,直接加载页面时会失败
问题 #9:服务函数校验错误会丢失结构
错误:inputValidator 的 Zod 错误被转换为字符串,导致客户端丢失结构
来源:GitHub Issue #6428
原因:TanStack Start 服务函数的错误序列化会将 Zod issues 数组转换为 error.message 中的 JSON 字符串,未经手动解析便无法使用。
预防措施:
// 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)
})
}
}
官方状态:已知问题,正在跟进修复 PR
问题 #10:useParams({ strict: false }) 返回未解析的值
错误:参数被标记为已解析类型,但导航后返回的却是字符串
来源:GitHub Issue #6385
原因:在 v1.147.3+ 中,使用 strict: false 时,match.params 不再进行解析。首次渲染正常,但导航后,值会以字符串形式存储,而非已解析的类型。
预防措施:
// 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)
}
官方状态:已知问题,需要使用变通方案
问题 #11:无路径路由的 notFoundComponent 未渲染
错误:无路径布局路由上的 notFoundComponent 被忽略
来源:GitHub Issue #6351、GitHub Issue #4065
原因:无路径路由(例如 routes/(authenticated)/route.tsx)不会渲染自己的 notFoundComponent,而是触发路由器配置中的 defaultNotFoundComponent。此问题自 2025 年 4 月起一直存在。
预防措施:
// ✗ 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>,
})
官方状态:已知问题,需要使用变通方案
问题 #12:已中止的加载器会使用未定义的错误渲染 errorComponent
错误:快速导航会中止先前的加载器,并使用 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 }) => {
// Check for undefined error (aborted request)
if (!error) {
return null // Or show loading state
}
return <div>Error: {error.message}</div>
},
})
官方状态:已知问题,需要使用变通方案
问题 #13:Vitest 无法读取 null 的属性(useState)
错误:使用 Vitest 运行测试时出现 Cannot read properties of null (reading 'useState')
来源:GitHub Issue #6262、PR #6074
原因:TanStack Start 的 tanstackStart() 插件与 Vitest 的 React Hooks 渲染冲突。这是一个已知的重复问题,目前已有 PR 在处理中。
预防措施:
// Temporary workaround: Comment out tanstackStart() for tests
// vite.config.ts
export default defineConfig({
plugins: [
// tanstackStart(), // Disable for tests
react(),
],
test: { environment: 'jsdom' },
})
官方状态:PR #6074 正在处理中,以修复此问题
问题 #14:流式 SSR 加载器中抛出错误会导致开发服务器崩溃
错误:路由加载器抛出错误但未等待时(使用 void 而非 await),开发服务器会崩溃
来源:GitHub Issue #6200
原因:SSR 流式模式无法处理未等待的 Promise 拒绝。错误会逸出加载器上下文,导致工作进程崩溃。
预防措施:
// ✗ 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
}
},
})
官方状态:已知问题,需要使用变通方案
问题 #15:筛选器返回零条结果时,预渲染会无限期挂起
错误:prerender.filter 返回零个路由时,构建步骤会挂起
来源:GitHub Issue #6425
原因:TanStack Start 预渲染无法妥善处理空路由集——它会无限期等待永远不会到来的路由。
预防措施:
// ✗ 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 },
})
官方状态:已知问题,需要使用变通方案
问题 #16:Docker 中无法预渲染
错误:预渲染步骤中出现“Unable to connect”,导致 Docker 中的构建失败
来源:GitHub Issue #6275、PR #6305
原因:用于预渲染的 Vite 预览服务器在 Docker 环境中无法访问。
预防措施:
// 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(),
],
})
官方状态:PR #6305 正在处理中
问题 #17:路由头部函数在加载器完成前执行
错误:head() 在 loader() 之前运行时,生成的 Meta 标签使用了不完整的数据
来源:GitHub Issue #6221
原因:路由的 loader() 完成前,head() 函数就可能执行,导致 Meta 标签使用占位数据或未定义的数据。
预防措施:
// ✗ 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 }],
}
},
})
官方状态:已知问题,需要使用变通方案
问题 #18:虚拟路由不支持手动延迟加载(社区提供)
错误:虚拟路由中的 createLazyFileRoute 被自动替换为 createFileRoute
来源:GitHub Issue #6396
原因:虚拟文件路由仅针对自动代码拆分而设计。不支持手动延迟加载路由——插件会静默替换它们。
预防措施:
// 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
官方状态:按设计如此(文档中已说明)
问题 #19:NavigateOptions 类型安全不一致(社区提供)
错误:NavigateOptions 类型不像 useNavigate() 那样强制要求必需的参数
来源:TkDodo 的博客:TanStack Router 的美妙之处
原因:运行时钩子与类型辅助工具的类型定义有所不同。NavigateOptions 的限制较少。
预防措施:
// ✗ 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
已验证:已与 TanStack Query 维护者的分析进行交叉核对
问题 #20:路由路径缺少前导斜杠(社区提供)
错误:定义路径时未添加前导斜杠,导致路由匹配失败
来源:官方调试指南
原因:这是初学者常见的错误——使用 'about' 而不是 '/about' 会导致路由匹配失败。
预防措施:
// ✗ Wrong: Missing leading slash
export const Route = createFileRoute('about')({ /* ... */ })
// ✓ Correct: Always start with /
export const Route = createFileRoute('/about')({ /* ... */ })
已验证:官方文档中记录的常见调试问题
Cloudflare Workers 集成
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 个。