TanStack Router RouteApi 类详解:面向路由 ID 预绑定的类型安全 Hook 访问器与 getRouteApi 迁移指南
2026/9/14 8:12:06 网站建设 项目流程

TanStack Router RouteApi 类详解:面向路由 ID 预绑定的类型安全 Hook 访问器与 getRouteApi 迁移指南

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

本文围绕 RouteApiClass 文档展开,系统讲解 TanStack Router 中RouteApi类的定位、构造参数、实例 API 及其在@tanstack/router-core中的实现细节。读完本文,你将理解如何在不直接持有路由对象的文件(如代码分割后的子文件)中,以完全类型安全的方式消费某个路由的useParamsuseSearchuseLoaderData等 Hook,并掌握官方推荐的getRouteApi替代写法。

一、RouteApi类是什么

RouteApi类提供了一组常见 Hook 的类型安全版本,包括useParamsuseSearchuseRouteContextuseNavigateuseLoaderDatauseLoaderDeps。与直接调用这些 Hook 需要显式传入from路由 ID 不同,RouteApi实例在创建时就已预绑定到一个特定的路由 ID,并同时锁定该路由在已注册路由树中对应的全部类型(参数类型、search 类型、loader 数据类型、context 类型等)。

它解决的典型场景是:当路由对象无法在当前文件中直接导入时——例如代码分割文件、工具模块、跨文件复用的逻辑——你仍然可以按路由 ID 拿到完整的类型化路由 API,而无需重复手写from: '/invoices/$invoiceId'这样的字符串字面量。

注意(来自原文档的重要提示):该类已被弃用(deprecated),将在 TanStack Router 的下一个主版本中移除。官方建议使用getRouteApi函数替代,二者行为完全等价,详见本文 迁移指南 一节。

二、构造参数:构造函数选项

RouteApi的构造函数只接受一个参数:用于配置该RouteApi实例的options对象。

opts.routeId选项

  • 类型:string
  • 必填
  • 含义:RouteApi实例将绑定到该路由 ID

需要特别说明的是文档与源码之间的细微差异:文档的选项小节将其命名为opts.routeId,但官方示例与源码实现中,构造参数实际写作id字段。从 React 版构造函数实现 可以确认,解构出的键是id

// packages/react-router/src/route.tsx(简化引用) export class RouteApi< TId, TRouter extends AnyRouter = RegisteredRouter, > extends BaseRouteApi<TId, TRouter> { /** * @deprecated Use the `getRouteApi` function instead. */ constructor({ id }: { id: TId }) { super({ id }) } // ... }

因此实际使用时应传入{ id: '/posts' }

构造返回值

构造函数返回一个RouteApi实例,该实例已预绑定到构造时传入的路由 ID。

三、RouteApi实例上的 API

结合 RouteApiType 文档与 React 版实现,实例上可用的方法与组件如下(均以预绑定路由 ID 为前提):

数据类 Hook:useParams/useSearch/useRouteContext/useLoaderDeps/useLoaderData

这五个 Hook 均为对应通用 Hook(useParamsuseSearchuseRouteContextuseLoaderDepsuseLoaderData)的类型安全包装,签名为:

useParams<TSelected = TAllParams>(opts?: { select?: (params: TAllParams) => TSelected }): TSelected

其余四个方法结构相同,返回类型分别替换为TFullSearchSchemaTAllContextTLoaderDepsTLoaderData

  • opts.select(可选):若提供,则其返回值成为 Hook 返回值,并用于浅比较以决定是否触发父组件重渲染;
  • opts.structuralSharing(可选,boolean):配置select返回值是否启用结构化共享;
  • 未提供select时,返回完整的数据对象;若内部strictfalse,则返回放宽版本(可能为undefined)。

一个值得注意的实现细节:在 RouteApi 类内部,useLoaderDepsuseLoaderData的包装实现都显式注入了strict: false

useLoaderDeps: UseLoaderDepsRoute<TId> = (opts) => { return useLoaderDeps({ ...opts, from: this.id, strict: false } as any) } useLoaderData: UseLoaderDataRoute<TId> = (opts) => { return useLoaderData({ ...opts, from: this.id, strict: false } as any) }

这与 routeApi.test-d.tsx 中的类型测试互相印证:当以shouldThrow: false调用useParams/useSearch时,返回类型会放宽为| undefined,例如{ invoiceId: string } | undefined。也就是说,通过RouteApi预绑定后,"路由未匹配"这一边界情况由类型系统如实表达,而非抛错。

useMatch

useMatch<TSelected = TAllContext>(opts?: { select?: (match: TAllContext) => TSelected }): TSelected

useMatch的预绑定版本,返回完整RouteMatch对象或其select结果,同样支持selectstructuralSharing

useNavigate

useNavigate(): // navigate 函数

useNavigate的预绑定版本,其默认from被静态推导为当前路由的fullPath。从 实现源码 看,它通过useRouter()拿到运行时 router,再从router.routesById[this.id].fullPath动态解析出from

useNavigate = (): UseNavigateResult< RouteTypesById<TRouter, TId>['fullPath'] > => { const router = useRouter() return useNavigate({ from: router.routesById[this.id as string].fullPath }) }

类型测试 routeApi.test-d.tsx 断言了这一点:对/invoices/$invoiceId路由而言,navigate的静态from精确等于'/invoices/$invoiceId'

redirectnotFound

这两个方法来自框架无关的基类BaseRouteApi

// packages/router-core/src/route.ts export class BaseRouteApi<TId, TRouter extends AnyRouter = RegisteredRouter> { id: TId constructor({ id }: { id: TId }) { this.id = id } notFound = (opts?: NotFoundError) => { return notFound({ routeId: this.id as string, ...opts }) } redirect: RedirectFnRoute<RouteTypesById<TRouter, TId>['fullPath']> = ( opts, ) => redirect({ from: this.id as string, ...opts } as any) }
  • redirect(opts?)redirect函数的类型安全版,from自动设为路由 ID,从而支持相对路径重定向,返回可在beforeLoadloader中抛出的Redirect对象;
  • notFound(opts?):等价于notFound({ routeId: this.id, ...opts }),用于抛出绑定到当前路由的 404 错误。

官方示例:

import { getRouteApi } from '@tanstack/react-router' const routeApi = getRouteApi('/dashboard/settings') export const Route = createFileRoute('/dashboard/settings')({ beforeLoad: ({ context }) => { if (!context.user) { // 类型安全重定向:from 自动为 '/dashboard/settings' throw routeApi.redirect({ to: '../login', // 相对路径,跳向兄弟路由 }) } }, })

Link

React 版的RouteApi还额外提供了一个预绑定的Link组件(见 实现),内部通过forwardRef包装Link,并自动从运行时 router 中解析出该路由的fullPath作为from,因此<Link>的目标路径校验与参数类型同样完全类型安全。

四、使用示例

原文档给出的示例(new RouteApi写法):

import { RouteApi } from '@tanstack/react-router' const routeApi = new RouteApi({ id: '/posts' }) export function PostsPage() { const posts = routeApi.useLoaderData() // ... }

结合类型测试中更完整的 路由树与数据定义,一个带参数、search 校验与 loader 的完整用法如下:

import { createRootRoute, createRoute, createRouter, getRouteApi, } from '@tanstack/react-router' const rootRoute = createRootRoute() const invoiceRoute = createRoute({ getParentRoute: () => rootRoute, path: '/invoices/$invoiceId', validateSearch: () => ({ page: 0 }), beforeLoad: () => ({ beforeLoadContext: 0 }), loaderDeps: () => ({ dep: 0 }), loader: () => ({ data: 0 }), }) const routeTree = rootRoute.addChildren([invoiceRoute]) const router = createRouter({ routeTree }) // 在无法导入路由对象的文件中,按 ID 获取类型化 API const invoiceApi = getRouteApi('/invoices/$invoiceId') function InvoicePage() { const { invoiceId } = invoiceApi.useParams() // 精确为 { invoiceId: string } const { page } = invoiceApi.useSearch() // 精确为 { page: number } const { data } = invoiceApi.useLoaderData() // 精确为 { data: number } return <div>{invoiceId} / page {page} / {data}</div> }

上述每个返回类型并非推断臆测,而是由 routeApi.test-d.tsx 中的expectTypeOf(...).toEqualTypeOf<...>()断言逐一验证的:useParams{ invoiceId: string }useRouteContext{ beforeLoadContext: number }useSearch{ page: number }useLoaderData{ data: number }

五、源码视角:预绑定是如何实现的

从源码结构看,React 版RouteApi的每个方法本质都是"把调用者传入的选项与from: this.id合并后,转发给对应的通用 Hook",例如 useSearch 的包装:

useSearch: UseSearchRoute<TId> = (opts) => { return useSearch({ ...opts, from: this.id } as any) }

类型侧的强度则来自两个泛型:TId(路由 ID 字面量)与TRouter(默认RegisteredRouter,即全局已注册的路由树类型)。实例方法签名中大量出现的RouteTypesById<TRouter, TId>就是"从已注册路由树中按 ID 抽取该路由全部类型信息"的工具类型——参数、search schema、loader 返回、fullPath 均由它推导。因此只要路由 ID 是路由树中已注册的合法字面量,RouteApi上的所有调用都享受与直接写在路由对象上等价的类型检查;写错 ID 会在编译期被ConstrainLiteral约束拦截(见下文getRouteApi的签名)。

BaseRouteApi(承载idnotFoundredirect)位于框架无关的 router-core,因此同一套预绑定语义在 React、Vue、Solid 三个适配层中保持一致:

  • React:packages/react-router/src/route.tsx
  • Vue:packages/vue-router/src/route.ts
  • Solid:packages/solid-router/src/route.tsx

六、从new RouteApi迁移到getRouteApi

如开头弃用提示所述,RouteApi类的构造函数在源码中已被标记@deprecated(见 route.tsx 构造函数注释)。替代函数 getRouteApi 的签名与实现非常简洁——它只是RouteApi的工厂封装:

// packages/react-router/src/route.tsx(简化引用) export function getRouteApi< const TId, TRouter extends AnyRouter = RegisteredRouter, >(id: ConstrainLiteral<TId, RouteIds<TRouter['routeTree']>>) { return new RouteApi<TId, TRouter>({ id }) }

迁移只需两步:将import { RouteApi }改为import { getRouteApi },将new RouteApi({ id: '/posts' })改为getRouteApi('/posts')。函数形式额外通过ConstrainLiteral<TId, RouteIds<...>>把路由 ID 约束为已注册路由树中的合法 ID 字面量,进一步压缩了手误空间。

import { getRouteApi } from '@tanstack/react-router' const routeApi = getRouteApi('/posts') export function PostsPage() { const posts = routeApi.useLoaderData() // ... }

七、小结

  • RouteApi是"预绑定路由 ID + 已注册路由类型"的类型安全 Hook 访问器,适用于无法直接导入路由对象的文件;
  • 构造参数为{ id }(文档选项小节写作routeId,以源码与官方示例的id为准),返回预绑定的RouteApi实例;
  • 实例提供useMatchuseRouteContextuseSearchuseParamsuseLoaderDepsuseLoaderDatauseNavigate,以及来自BaseRouteApiredirectnotFound,React 版还有预绑定的Link
  • useLoaderData/useLoaderDeps内部固定strict: false,类型上以| undefined表达未匹配场景,并有专门的类型测试保障;
  • 该类已弃用,官方推荐getRouteApi(routeId),行为等价且 ID 约束更强,将在下个主版本移除RouteApi类,建议新代码直接使用函数形式。

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询