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中的实现细节。读完本文,你将理解如何在不直接持有路由对象的文件(如代码分割后的子文件)中,以完全类型安全的方式消费某个路由的useParams、useSearch、useLoaderData等 Hook,并掌握官方推荐的getRouteApi替代写法。
一、RouteApi类是什么
RouteApi类提供了一组常见 Hook 的类型安全版本,包括useParams、useSearch、useRouteContext、useNavigate、useLoaderData和useLoaderDeps。与直接调用这些 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(useParams、useSearch、useRouteContext、useLoaderDeps、useLoaderData)的类型安全包装,签名为:
useParams<TSelected = TAllParams>(opts?: { select?: (params: TAllParams) => TSelected }): TSelected其余四个方法结构相同,返回类型分别替换为TFullSearchSchema、TAllContext、TLoaderDeps、TLoaderData。
opts.select(可选):若提供,则其返回值成为 Hook 返回值,并用于浅比较以决定是否触发父组件重渲染;opts.structuralSharing(可选,boolean):配置select返回值是否启用结构化共享;- 未提供
select时,返回完整的数据对象;若内部strict为false,则返回放宽版本(可能为undefined)。
一个值得注意的实现细节:在 RouteApi 类内部,useLoaderDeps与useLoaderData的包装实现都显式注入了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 }): TSelecteduseMatch的预绑定版本,返回完整RouteMatch对象或其select结果,同样支持select与structuralSharing。
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'。
redirect与notFound
这两个方法来自框架无关的基类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,从而支持相对路径重定向,返回可在beforeLoad或loader中抛出的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(承载id、notFound、redirect)位于框架无关的 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实例; - 实例提供
useMatch、useRouteContext、useSearch、useParams、useLoaderDeps、useLoaderData、useNavigate,以及来自BaseRouteApi的redirect、notFound,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),仅供参考