关于 tanstack-router
tanstack-router 是用于在 React SPA 中配置和扩展 TanStack Router 的技能。内容包括类型安全路由、经过验证的 URL 搜索状态、加载器、路由层的 TanStack Query 集成及路由组件测试。
主要功能
- 01类型安全的路由定义与导航
- 02基于文件和代码的路由
- 03经过验证的搜索参数
- 04路由加载器与预加载
- 05TanStack Query 加载器集成
- 06内存路由组件测试
适用场景
- 01在 React SPA 中配置路由
- 02添加类型化路由和搜索状态
- 03使用 TanStack Query 预取路由数据
- 04测试依赖路由器的组件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ bm629/ agent-skills --skill tanstack-router安装后在 Claude Code 中输入/tanstack-router确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/auth-and-testing.mdnavigation-and-splitting.mdquery-integration.mdrouting-approaches.mdsearch-and-loaders.mdsources.md详细介绍
tanstack-router — SKILL.md
**变体:**标准 · **使用时机:**调用此技能后,你会阅读它,并连接或扩展 TanStack Router 应用,然后将控制权交还给调用方。
已根据
@tanstack/react-routerv1(1.170.x)和截至 2026-06 的 TanStack Router 文档进行验证。 TanStack Router 处于 v1 主版本(持续发布 semver 次版本)。以下 API 已在技能编写时对照官方tanstack.com/router文档核验。请将确切的选项名称视为与版本绑定:如果某个名称在你安装的版本中返回 404,请查看references/sources.md中列出的该版本文档页面。
概述
TanStack Router 是适用于 React SPA 的完全类型安全的客户端路由器。它的独特之处在于,路由树是类型的来源:通过 TypeScript 声明合并注册路由器后,每个 Link、navigate、useParams、useSearch 和 useLoaderData 都会根据实际路由进行检查——无效路径和缺失参数会导致编译错误。它内置了一流的搜索参数(由模式验证的 URL 状态)、路由加载器(渲染前获取数据,并支持预加载)和代码拆分。此技能会搭建路由器、以类型安全的方式连接路由,并介绍加载器与数据获取之间的衔接(包括 TanStack Query 的协作方式),以及用于路由组件的测试工具。它是路由层;全栈框架 TanStack Start 构建于其之上(不在本文范围内——请参阅边界)。
何时启用
- ✅ 在 Vite + React + TypeScript SPA 中设置路由(或扩展现有的 TanStack Router 应用)。
- ✅ 定义路由、类型化导航、经验证的搜索参数或路由加载器。
- ✅ 将路由加载器连接到 TanStack Query(在加载器中预取,在组件中读取)。
- ✅ 编写用于渲染路由组件的测试(需要内存历史记录测试工具)。
请勿在以下情况启用:
- 项目使用 React Router(
react-router/ Remix)——这是另一个路由器;此技能不适用于它。 - 需要 TanStack Start SSR / 服务端函数 / 流式反序列化——Start 是下游使用方,本技能不介绍它。
- 需要脱离路由使用的 TanStack Query 机制(缓存配置、变更、与路由无关的无限查询)——这属于
tanstack-query技能的职责;此技能仅介绍通过路由加载器/上下文使用查询。
工作流
步骤 1:安装并连接打包工具插件
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin # file-based routing (Vite/Rspack/Webpack)
npm install -D @tanstack/react-router-devtools # optional, dev-only
将插件添加到 Vite。当前导出项是来自 @tanstack/router-plugin/vite 的 tanstackRouter(驼峰命名),必须在 @vitejs/plugin-react 之前运行:
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
tanstackRouter({ target: 'react', autoCodeSplitting: true }),
react(),
],
})
该插件会监视 src/routes/ 并生成 src/routeTree.gen.ts。(旧名称 TanStackRouterVite 和 @tanstack/router-vite-plugin 包仍可通过别名使用,但 tanstackRouter 是当前名称。请参阅 references/sources.md。)
步骤 2:类型安全设置(createRouter + RouterProvider + Register)
这是让整个应用的导航具备类型安全性的连接方式。Register 声明合并是必需的——没有它,类型化钩子会退化为联合类型,并失去自动补全和编译检查。
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({
routeTree,
defaultPreload: 'intent', // preload on hover/focus; see Step 6
scrollRestoration: true,
})
// Declaration merging: binds YOUR router's type into the library's hooks.
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
步骤 3:根路由
// src/routes/__root.tsx
import { createRootRoute, Outlet, Link } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<nav>
<Link to="/">Home</Link>{' '}
<Link to="/posts">Posts</Link>
</nav>
<Outlet />
<TanStackRouterDevtools />
</>
),
})
__root.tsx 会包装每条路由;<Outlet /> 会渲染匹配的子路由。若要使用类型化路由器上下文(例如注入 queryClient),请改用 createRootRouteWithContext<T>()——请参阅 references/auth-and-testing.md 和 references/query-integration.md。
步骤 4:完整路由示例——基于文件的路由(推荐默认方式)
基于文件的路由是 TanStack 的默认方式,也是此处的主要路径。文件的位置决定其 URL;createFileRoute('/path') 会先接收路径字符串,再接收选项对象。一个包含路径参数、已验证搜索参数、加载器和组件的完整路由:
// src/routes/posts.$postId.tsx -> matches /posts/$postId
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'
const postSearchSchema = z.object({
tab: z.enum(['body', 'comments']).catch('body'),
})
export const Route = createFileRoute('/posts/$postId')({
validateSearch: zodValidator(postSearchSchema), // Zod v3 adapter; Zod v4: pass schema directly
loader: async ({ params }) => fetchPost(params.postId),
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams() // { postId: string } — typed
const { tab } = Route.useSearch() // { tab: 'body' | 'comments' } — typed
const post = Route.useLoaderData() // typed to the loader's return
return <article data-tab={tab}>{post.title}</article>
}
Route.useParams() / Route.useSearch() / Route.useLoaderData() 是文件内的类型化钩子——无需 from 参数,因为路由对象本身就是数据来源。在其他文件中使用 useParams({ from: '/posts/$postId' }) 或 getRouteApi('/posts/$postId')(参见 references/search-and-loaders.md)。
基于代码的路由(通过
getParentRoute+addChildren+ 手动createRouter使用createRootRoute/createRoute)也受到全面支持,并在references/routing-approaches.md中作为一等方式介绍——当你无法运行打包器插件时,请使用它。
步骤 5:导航要点
import { Link, useNavigate, redirect } from '@tanstack/react-router'
// Declarative — renders a real <a href>; preloads on intent.
<Link to="/posts/$postId" params={{ postId: '123' }} search={{ tab: 'comments' }}
activeProps={{ className: 'active' }}>Post 123</Link>
// Imperative — for side effects (after a form submit, etc.).
const navigate = useNavigate()
navigate({ to: '/posts', search: (prev) => ({ ...prev, tab: 'body' }) })
// Redirect — THROW it from beforeLoad/loader (never navigate in a component for guards).
throw redirect({ to: '/login', search: { redirect: location.href } })
完整功能——activeOptions、渲染属性 isActive、导航阻止(useBlocker)、路由遮蔽——见 references/navigation-and-splitting.md。
步骤 6:从加载器到数据的衔接(以及 TanStack Query 握手)
路由加载器会在组件渲染之前运行,因此组件挂载时数据已就绪,并可根据链接意图进行预加载。与 TanStack Query 集成的推荐方式:定义一个 queryOptions 工厂,在加载器中调用 ensureQueryData,然后在组件中通过 useSuspenseQuery 读取数据。
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { postQueryOptions } from '../queries' // a queryOptions factory
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context: { queryClient }, params }) =>
queryClient.ensureQueryData(postQueryOptions(params.postId)),
component: Post,
})
function Post() {
const { postId } = Route.useParams()
const { data: post } = useSuspenseQuery(postQueryOptions(postId)) // cache hit — no refetch
return <article>{post.title}</article>
}
queryClient 通过路由器上下文传入加载器(createRootRouteWithContext<{ queryClient: QueryClient }>() + createRouter({ context: { queryClient } }))。完整集成方式——等待与延迟加载、defaultPreloadStaleTime: 0、通过 router.invalidate() 在变更后失效,以及指向规范 tanstack-query 技能的交叉引用——见 references/query-integration.md。此处不会讲解 TanStack Query 自身的机制(抽象层面的查询键、变更、缓存调优);这些内容属于 tanstack-query 技能。
步骤 7:测试路由组件
要在测试中渲染路由,而无需浏览器 URL,请构建一个内存路由器并使用 RouterProvider 包装。本技能负责提供这个测试框架;组件测试技能会使用它。
import { render, screen } from '@testing-library/react'
import { createMemoryHistory, createRouter, RouterProvider, createRootRoute, createRoute } from '@tanstack/react-router'
function renderRoute(initialPath: string) {
const rootRoute = createRootRoute()
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', component: HomePage })
const routeTree = rootRoute.addChildren([indexRoute])
const router = createRouter({
routeTree,
history: createMemoryHistory({ initialEntries: [initialPath] }),
})
render(<RouterProvider router={router} />)
}
使用 Testing Library 的 findBy* 等待渲染结果(路由器会在匹配的组件出现前解析加载器)。通过 router.navigate(...) 驱动导航。完整测试框架——类型化测试路由器工厂、基于文件的路由树变体、等待 router.load()——见 references/auth-and-testing.md。
规则
硬性规则(绝不违反):
- 始终添加
Register声明合并。 如果没有declare module '@tanstack/react-router' { interface Register { router: typeof router } },所有类型化 Hook 都会退化为所有路由的联合类型,你也会失去编译时路由检查。 - 将身份验证/重定向守卫放在
beforeLoad(或loader)中,绝不要放在组件中。 从beforeLoad抛出redirect(...)会在路由渲染前将其拦截;在组件中进行守卫则会先闪现受保护的内容。 - 只要 loader 依赖搜索参数,就使用
loaderDeps。 除非在loaderDeps中声明依赖,否则搜索参数变化时 loader 不会重新运行;省略它会导致提供过期数据。 - 通过
validateSearch验证每个搜索参数。 搜索参数是用户可控的 URL 输入;未经验证就读取会留下any类型漏洞并带来运行时风险。 - 在自身路由文件之外使用类型化 Hook 时,传入
from(或使用Route.*/getRouteApi)。 在共享组件中直接调用useParams()会返回联合类型,而不是该路由的精确类型。 - 通过路由来处理查询机制。 本技能仅介绍 TanStack Query 在 loader/上下文交界处的用法,并将
tanstack-query作为规范参考;不要将独立的查询模式搬进路由代码。
偏好(可覆盖):
- 优先采用基于文件的路由(
tanstackRouter插件和createFileRoute);只有无法运行打包工具插件时才使用基于代码的路由。 - 用户主动发起导航时,优先使用
<Link>,而不是useNavigate(真实的<a href>、鼠标中键打开、预加载、无障碍支持);仅在副作用场景使用useNavigate。 - 将
defaultPreload设为'intent',实现流畅导航;当缓存由 TanStack Query 管理时,将defaultPreloadStaleTime设为0。 - 将
loader保留在初始打包文件中(不要对它进行代码拆分),除非有明确理由;拆分会在获取数据前增加一次服务器往返。
易踩问题
TanStackRouterVite/@tanstack/router-vite-plugin看起来没问题,但它们是旧名称。 当前导出项是来自@tanstack/router-plugin/vite的tanstackRouter。旧名称仍有别名支持,因此代码虽然“能运行”,却会逐渐偏离文档;请使用当前名称。routeTree.gen.ts是生成文件——绝不要手动编辑,并且必须在createRouter之前存在。 如果导入./routeTree.gen失败,说明插件尚未运行(启动开发服务器/构建),或它在 Vite 配置中的顺序排在react()之后。- loader 无提示地提供过期数据,几乎总是因为缺少
loaderDeps。 除非通过loaderDeps: ({ search }) => ({ page: search.page })声明依赖,否则更改?page=2不会让 loader 重新运行。 - 共享组件中的
useParams()类型“失效”。 如果没有from,它会联合所有路由的参数。请使用from、Route.useParams()(同一文件)或getRouteApi(path)(拆分文件)。 - Zod 适配器版本陷阱。 对于 Zod v3,请使用
@tanstack/zod-adapter中的zodValidator进行包装;对于 Zod v4,则不需要适配器——将 schema 直接传给validateSearch。旧的@tanstack/router-zod-adapter包已弃用。 - 延迟数据:返回原始 Promise,不要对它使用
await。 当前文档会从 loader 返回一个未等待的 Promise,并通过<Await>/useAwaited使用它。独立的defer()助手主要用于 SSR/流式序列化(Start),在 SPA 中并非必需——参见references/search-and-loaders.md。 - 没有 loader 预取时,
useSuspenseQuery会在首次绘制时挂起。 在 loader 中使用ensureQueryData的意义在于,组件的useSuspenseQuery可以命中缓存。跳过 loader 就会重新引入 loader 原本要消除的瀑布式请求。
反模式
- “我会在
useEffect中获取数据,而不是使用 loader。” 这会再次引入先渲染再获取数据的瀑布流,并使预加载失效——loader 才是关键。 - “我会在组件内通过重定向来保护路由。” 这会导致受保护内容一闪而过,还会多一次渲染。应在
beforeLoad中进行保护。 - “我会跳过
validateSearch,直接读取原始的useSearch()。” 这是来自 URL、未经验证的any;URL 格式错误会导致运行时崩溃。 - “既然要集成 TanStack Query,我就在这里介绍完整的 TanStack Query 缓存/变更 API。” 这会造成范围蔓延——应从路由的角度来讲解,并指向
tanstack-query。 - “既然路由器支持 TanStack Start 的 SSR/流式传输,我就把它们写进文档。” Start 是独立产品(下游使用方);应将其排除在外。
输出
此技能生成可运行的 TanStack Router 代码:vite.config.ts 插件入口、router.tsx(createRouter + Register)、RouterProvider 接线、路由模块(以基于文件的方式为主,也支持基于代码的方式)、类型化导航和钩子、经过验证的搜索参数、与 TanStack Query 接线的 loader,以及内存历史测试框架。抽象的使用方是在 React SPA 中搭建路由的工程师或编码 Agent,以及复用该测试框架的同级组件测试工作流。
相关内容
tanstack-query— TanStack Query 机制(查询键、queryOptions、变更、缓存调优)的权威来源。此技能会在 loader/上下文的交界处引用它,不会重复介绍。vite— 运行@tanstack/router-plugin/vite插件并生成routeTree.gen.ts的开发服务器和打包工具。react-component-testing(同级)— 使用此技能负责的路由器测试框架(步骤 7/references/auth-and-testing.md)。typescript-typecheck—Register扩展和类型化钩子依赖严格的tsconfig;路由树类型检查由tsc强制执行。- 边界(仅说明名称,不作讲解): TanStack Start(SSR/流式传输全栈框架——下游使用方);React Router(
react-router/Remix——此技术栈未选择的替代方案)。
渐进式披露
较大的内容存放在 references/ 中,按需加载:
references/routing-approaches.md— 选择或实现路由结构时加载:完整的文件命名约定表(__root、index、$param、_pathless、(group)、$通配符、点号表示法、尾随下划线取消嵌套),以及完整的基于代码的实现方式(createRoute+getParentRoute+addChildren+ 手动调用createRouter),并附带使用时机对比。references/search-and-loaders.md— 深入了解搜索参数和数据加载时加载:使用可插拔验证器的validateSearch(包含 Zod 适配器的完整示例,并介绍 Valibot/自定义验证器)、读取/更新搜索参数、父级→子级继承、自定义序列化、搜索中间件(retainSearchParams/stripSearchParams);加载器、loaderDeps、延迟数据(原始 Promise +Await)、并行加载、上下文加载。references/navigation-and-splitting.md— 处理导航和包体积时加载:Link选项、useNavigate、redirect、useBlocker、代码拆分(createLazyFileRoute/autoCodeSplitting)、预加载(defaultPreload的取值、preloadDelay、过期时间)、滚动恢复、路由遮罩。references/auth-and-testing.md— 处理身份验证模式或测试时加载:createRootRouteWithContext、无路径的_authenticated布局、beforeLoad重定向守卫、notFound()、head/meta;以及完整的路由器测试框架(内存历史记录工厂、基于文件的路由树变体、等待路由器就绪)。references/query-integration.md— 了解 TanStack Query 的衔接点时加载:ensureQueryData+useSuspenseQuery的协作、等待与延迟的取舍、queryOptions工厂、变更后通过router.invalidate()进行失效处理、staleTime/gcTime/defaultPreloadStaleTime— 均从路由角度进行说明,并交叉引用规范的tanstack-query技能。references/sources.md— 研究出处(官方tanstack.com/router文档页面 + 在 forge 验证的 npm 版本)。
此技能不包含 scripts/ 或 assets/。
正文预算
description≤ 1,024 个字符(已遵守)。- 正文 ≤ 约 500 行/5,000 个 token — 每轮都会保留在上下文中;较长的内容存放在
references/中。 - 每个参考文件:超过 10k 个 token 时警告,超过 25k 个 token 时错误。参考文件总量:超过 25k 个 token 时警告,超过 50k 个 token 时错误。