TanStack Router 的构建目标是在 TypeScript 编译器和运行时的限制内尽可能实现类型安全。这意味着它不仅是用 TypeScript 编写的,而且还**完全推断它提供的类型,并坚定地将其贯穿整个路由体验**。
最终,这意味着作为开发者,您编写**更少的类型**,并且随着代码的演进,您对**代码更有信心**。
路由是分层的,它们的定义也是如此。如果您正在使用基于文件的路由,那么大部分类型安全问题已经为您处理好了。
如果您直接使用 Route 类,您需要了解如何使用 Route 的 getParentRoute 选项来确保您的路由被正确地类型化。这是因为子路由需要知道**所有**父路由的类型。没有这个,那些您从 layout 和 pathless layout 路由(上溯 3 级)中解析出的宝贵的搜索参数将会消失在 JS 的虚空中。
所以,不要忘记将父路由传递给您的子路由!
const parentRoute = createRoute({
getParentRoute: () => parentRoute,
})
const parentRoute = createRoute({
getParentRoute: () => parentRoute,
})
为了使您的路由器的类型能够与顶级导出(如 Link、useNavigate、useParams 等)一起工作,它们必须渗透到 type-script 模块边界并注册到库中。为此,我们在导出的 Register 接口上使用声明合并。
const router = createRouter({
// ...
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
const router = createRouter({
// ...
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
通过将您的路由器注册到模块中,您现在可以使用导出的 hooks、组件和实用程序,并具有您路由器的精确类型。
组件上下文是 React 和其他框架中一个很棒的工具,用于为组件提供依赖项。但是,如果上下文在组件层次结构中移动时类型发生变化,TypeScript 将无法知道如何推断这些变化。为了解决这个问题,基于上下文的 hooks 和组件要求您给出关于它们如何以及在何处被使用的提示。
export const Route = createFileRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
// Each route has type-safe versions of most of the built-in hooks from TanStack Router
const params = Route.useParams()
const search = Route.useSearch()
// Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
// we must pass the `from` param to tell the hook our relative position in the route hierarchy.
const navigate = useNavigate({ from: Route.fullPath })
// ... etc
}
export const Route = createFileRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
// Each route has type-safe versions of most of the built-in hooks from TanStack Router
const params = Route.useParams()
const search = Route.useSearch()
// Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
// we must pass the `from` param to tell the hook our relative position in the route hierarchy.
const navigate = useNavigate({ from: Route.fullPath })
// ... etc
}
每个需要上下文提示的 hook 和组件都将有一个 from 参数,您可以在其中传递您正在其中渲染的路由的 ID 或路径。
🧠 快速提示:如果您的组件是代码分割的,您可以使用 getRouteApi 函数 来避免必须传入 Route.fullPath 才能访问类型化的 useParams() 和 useSearch() hooks。
from 属性是可选的,这意味着如果您不传递它,您将获得路由器对可用类型的最佳猜测。通常,这意味着您将获得路由器中所有路由的所有类型的联合类型。
从技术上讲,可以传递一个满足 TypeScript 的 from,但可能与您在运行时渲染的实际路由不匹配。在这种情况下,每个支持 from 的 hook 和组件都会检测到您的期望是否与您在其中渲染的实际路由不匹配,并将抛出一个运行时错误。
如果您正在渲染一个在多个路由之间共享的组件,或者您正在渲染一个不在路由内的组件,您可以传递 strict: false 而不是 from 选项。这不仅会消除运行时错误,而且还会为您提供宽松但准确的类型,用于您正在调用的潜在 hook。一个很好的例子是从共享组件调用 useSearch
function MyComponent() {
const search = useSearch({ strict: false })
}
function MyComponent() {
const search = useSearch({ strict: false })
}
在这种情况下,search 变量的类型将是路由器中所有路由的所有可能搜索参数的联合类型。
Router 上下文非常有用,因为它是最终的分层依赖注入。您可以向路由器以及它渲染的每个路由提供上下文。在构建此上下文时,TanStack Router 会将其与路由层次结构合并,以便每个路由都可以访问其所有父级的上下文。
createRootRouteWithContext 工厂使用实例化的类型创建一个新的路由器,然后会要求您履行与您的路由器相同的类型约定,并确保您的上下文在整个路由树中被正确类型化。
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
component: App,
})
const routeTree = rootRoute.addChildren([
// ... all child routes will have access to `whateverYouWant` in their context
])
const router = createRouter({
routeTree,
context: {
// This will be required to be passed now
whateverYouWant: true,
},
})
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
component: App,
})
const routeTree = rootRoute.addChildren([
// ... all child routes will have access to `whateverYouWant` in their context
])
const router = createRouter({
routeTree,
context: {
// This will be required to be passed now
whateverYouWant: true,
},
})
随着您的应用程序扩展,TypeScript 检查时间自然会增加。当您的应用程序扩展时,需要记住一些事项以保持 TS 检查时间缩短。
客户端数据缓存(TanStack Query 等)的一个很棒的模式是预取数据。例如,使用 TanStack Query,您可能有一个路由在 loader 中调用 queryClient.ensureQueryData。
export const Route = createFileRoute('/posts/$postId/deep')({
loader: ({ context: { queryClient }, params: { postId } }) =>
queryClient.ensureQueryData(postQueryOptions(postId)),
component: PostDeepComponent,
})
function PostDeepComponent() {
const params = Route.useParams()
const data = useSuspenseQuery(postQueryOptions(params.postId))
return <></>
}
export const Route = createFileRoute('/posts/$postId/deep')({
loader: ({ context: { queryClient }, params: { postId } }) =>
queryClient.ensureQueryData(postQueryOptions(postId)),
component: PostDeepComponent,
})
function PostDeepComponent() {
const params = Route.useParams()
const data = useSuspenseQuery(postQueryOptions(params.postId))
return <></>
}
这看起来可能很好,对于小的路由树,您可能不会注意到任何 TS 性能问题。但是,在这种情况下,TS 必须推断加载器的返回类型,尽管它永远不会在您的路由中使用。如果加载器数据是一个复杂类型,其中有许多路由以这种方式预取,则可能会降低编辑器性能。在这种情况下,更改非常简单,让 typescript 推断 Promise
export const Route = createFileRoute('/posts/$postId/deep')({
loader: async ({ context: { queryClient }, params: { postId } }) => {
await queryClient.ensureQueryData(postQueryOptions(postId))
},
component: PostDeepComponent,
})
function PostDeepComponent() {
const params = Route.useParams()
const data = useSuspenseQuery(postQueryOptions(params.postId))
return <></>
}
export const Route = createFileRoute('/posts/$postId/deep')({
loader: async ({ context: { queryClient }, params: { postId } }) => {
await queryClient.ensureQueryData(postQueryOptions(postId))
},
component: PostDeepComponent,
})
function PostDeepComponent() {
const params = Route.useParams()
const data = useSuspenseQuery(postQueryOptions(params.postId))
return <></>
}
这样,加载器数据永远不会被推断,并且它将推断移出路由树,直到您第一次使用 useSuspenseQuery。
考虑以下 Link 的用法
<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />
<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />
这些示例对 TS 性能不利。这是因为 search 解析为所有路由的所有 search 参数的联合类型,并且 TS 必须检查您传递给 search 属性的任何内容是否与这个可能很大的联合类型匹配。随着您的应用程序增长,此检查时间将与路由和搜索参数的数量成线性增长。我们已尽力优化这种情况(TypeScript 通常会执行一次此工作并缓存它),但对这个大型联合类型的初始检查代价很高。这也适用于 params 和其他 API,例如 useSearch、useParams、useNavigate 等。
相反,您应该尝试使用 from 或 to 缩小到相关的路由。
<Link from={Route.fullPath} to=".." search={{page: 0}} />
<Link from="/posts" to=".." search={{page: 0}} />
<Link from={Route.fullPath} to=".." search={{page: 0}} />
<Link from="/posts" to=".." search={{page: 0}} />
请记住,您可以始终将联合类型传递给 to 或 from,以缩小您感兴趣的路由范围。
const from: '/posts/$postId/deep' | '/posts/' = '/posts/'
<Link from={from} to='..' />
const from: '/posts/$postId/deep' | '/posts/' = '/posts/'
<Link from={from} to='..' />
您还可以将分支传递给 from,以仅将 search 或 params 解析为来自该分支的任何后代
const from = '/posts'
<Link from={from} to='..' />
const from = '/posts'
<Link from={from} to='..' />
/posts 可以是一个分支,它有许多后代,这些后代共享相同的 search 或 params
路由通常具有 params、search、loaders 或 context,它们甚至可以引用外部依赖项,这些依赖项也会对 TS 推断造成很大的负担。对于此类应用程序,使用对象创建路由树可能比使用元组更高效。
createChildren 也可以接受一个对象。对于具有复杂路由和外部库的大型路由树,对象在 TS 类型检查方面可能比大型元组快得多。性能提升取决于您的项目,您拥有的外部依赖项以及这些库的类型编写方式
const routeTree = rootRoute.addChildren({
postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
indexRoute,
})
const routeTree = rootRoute.addChildren({
postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
indexRoute,
})
请注意,此语法更冗长,但具有更好的 TS 性能。使用基于文件的路由,路由树是为您生成的,因此冗长的路由树不是问题
通常您可能想要重用公开的类型。例如,您可能会尝试像这样使用 LinkProps
const props: LinkProps = {
to: '/posts/',
}
return (
<Link {...props}>
)
const props: LinkProps = {
to: '/posts/',
}
return (
<Link {...props}>
)
这对 TS 性能非常不利。这里的问题是 LinkProps 没有类型参数,因此是一个非常大的类型。它包括 search,它是所有 search 参数的联合类型,它包含 params,它是所有 params 的联合类型。将此对象与 Link 合并时,它将对此巨大类型进行结构比较。
相反,您可以使用 as const satisfies 来推断精确的类型,而不是直接使用 LinkProps 以避免巨大的检查
const props = {
to: '/posts/',
} as const satisfies LinkProps
return (
<Link {...props}>
)
const props = {
to: '/posts/',
} as const satisfies LinkProps
return (
<Link {...props}>
)
由于 props 的类型不是 LinkProps,因此此检查更便宜,因为类型更精确。您还可以通过缩小 LinkProps 的范围来进一步改进类型检查
const props = {
to: '/posts/',
} as const satisfies LinkProps<RegisteredRouter, string '/posts/'>
return (
<Link {...props}>
)
const props = {
to: '/posts/',
} as const satisfies LinkProps<RegisteredRouter, string '/posts/'>
return (
<Link {...props}>
)
这甚至更快,因为我们正在对照缩小的 LinkProps 类型进行检查。
您还可以使用它将 LinkProps 的类型缩小到特定类型,以用作函数的 prop 或参数
export const myLinkProps = [
{
to: '/posts',
},
{
to: '/posts/$postId',
params: { postId: 'postId' },
},
] as const satisfies ReadonlyArray<LinkProps>
export type MyLinkProps = (typeof myLinkProps)[number]
const MyComponent = (props: { linkProps: MyLinkProps }) => {
return <Link {...props.linkProps} />
}
export const myLinkProps = [
{
to: '/posts',
},
{
to: '/posts/$postId',
params: { postId: 'postId' },
},
] as const satisfies ReadonlyArray<LinkProps>
export type MyLinkProps = (typeof myLinkProps)[number]
const MyComponent = (props: { linkProps: MyLinkProps }) => {
return <Link {...props.linkProps} />
}
这比直接在组件中使用 LinkProps 更快,因为 MyLinkProps 是一个更精确的类型
另一种解决方案是不使用 LinkProps,而是提供控制反转来渲染一个缩小到特定路由的 Link 组件。Render props 是反转组件用户控制权的好方法
export interface MyComponentProps {
readonly renderLink: () => React.ReactNode
}
const MyComponent = (props: MyComponentProps) => {
return <div>{props.renderLink()}</div>
}
const Page = () => {
return <MyComponent renderLink={() => <Link to="/absolute" />} />
}
export interface MyComponentProps {
readonly renderLink: () => React.ReactNode
}
const MyComponent = (props: MyComponentProps) => {
return <div>{props.renderLink()}</div>
}
const Page = () => {
return <MyComponent renderLink={() => <Link to="/absolute" />} />
}
这个特定示例非常快,因为我们已经反转了我们导航到组件用户的控制权。Link 已缩小到我们想要导航到的确切路由
您的每周 JavaScript 新闻。每周一免费发送给超过 100,000 名开发者。