mcpField
T
tanstack-router

tanstack-router

github.com/jezweb开发1.1k799 次安装3 周前更新
代码仓库

关于 tanstack-router

TanStack Router 是面向 React SPA 的类型安全路由库,适用于需要文件路由和路由级数据加载的开发者。它支持 TanStack Query 集成、搜索参数验证和虚拟路由配置。

主要功能

  1. 01基于文件的类型安全路由
  2. 02路由级数据加载
  3. 03集成 TanStack Query 预取与缓存
  4. 04验证 URL 搜索参数
  5. 05路由级错误与未找到处理
  6. 06支持虚拟文件路由

适用场景

  1. 01为 React SPA 配置文件路由
  2. 02在页面渲染前加载路由数据
  3. 03校验并处理 URL 搜索参数
  4. 04组织复杂的嵌套路由和布局

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill tanstack-router

安装后在 Claude Code 中输入/tanstack-router确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
references/
common-errors.md
rules/
tanstack-router.md
templates/
package.json
route-examples.tsx
vite.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 个。

评论

支持 Markdown