无论你是否使用显式的相对路径语法(../../somewhere),应用内的所有导航本质上都是相对的。每当点击链接或执行命令式导航调用时,总会存在一个起点路径和一个目标路径,这意味着你始终是在从某个路由跳转至另一个路由。
TanStack Router 在设计所有导航时都秉持着相对导航的核心概念,因此你会在 API 中频繁看到两个属性:
⚠️ 如果未提供 from 路由路径,路由器会假设你从根路由 / 开始导航,此时仅自动补全绝对路径。毕竟,你需要知道起点才能确定去向 😉。
TanStack Router 中所有导航和路由匹配 API 都采用相同的核心接口,仅因 API 不同存在细微差异。这意味着你只需学习一次导航和路由匹配,就能在整个库中使用相同的语法和概念。
这是所有导航和路由匹配 API 共用的核心 ToOptions 接口:
type ToOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = {
// `from` 是可选的路由ID或路径。若不提供,则仅自动补全绝对路径并保持类型安全。通常为了方便会传入当前渲染路由的 route.fullPath。若不确定起点路由,请留空并使用绝对路径或不安全的相对路径。
from: string
// `to` 可以是绝对路径,或是基于 `from` 选项的相对路径。⚠️ 不要在 `to` 选项中直接拼接路径参数、哈希或搜索参数,应使用 `params`、`search` 和 `hash` 选项。
to: string
// `params` 可以是待插入 `to` 选项的路径参数对象,或是接收旧参数返回新参数的函数。这是将动态参数插入最终URL的唯一方式。根据 `from` 和 `to` 路由,可能需要提供部分或全部路径参数。TypeScript 会提示必需参数。
params:
| Record<string, unknown>
| ((prevParams: Record<string, unknown>) => Record<string, unknown>)
// `search` 可以是查询参数对象,或是接收旧搜索参数返回新参数的函数。根据路由可能需要提供部分或全部查询参数。TypeScript 会提示必需搜索参数。
search:
| Record<string, unknown>
| ((prevSearch: Record<string, unknown>) => Record<string, unknown>)
// `hash` 可以是字符串,或是接收旧哈希值返回新值的函数。
hash?: string | ((prevHash: string) => string)
// `state` 可以是状态对象,或是接收旧状态返回新状态的函数。状态存储在 history API 中,适合传递不想永久保存在URL搜索参数中的数据。
state?:
| Record<string, any>
| ((prevState: Record<string, unknown>) => Record<string, unknown>)
}
type ToOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = {
// `from` 是可选的路由ID或路径。若不提供,则仅自动补全绝对路径并保持类型安全。通常为了方便会传入当前渲染路由的 route.fullPath。若不确定起点路由,请留空并使用绝对路径或不安全的相对路径。
from: string
// `to` 可以是绝对路径,或是基于 `from` 选项的相对路径。⚠️ 不要在 `to` 选项中直接拼接路径参数、哈希或搜索参数,应使用 `params`、`search` 和 `hash` 选项。
to: string
// `params` 可以是待插入 `to` 选项的路径参数对象,或是接收旧参数返回新参数的函数。这是将动态参数插入最终URL的唯一方式。根据 `from` 和 `to` 路由,可能需要提供部分或全部路径参数。TypeScript 会提示必需参数。
params:
| Record<string, unknown>
| ((prevParams: Record<string, unknown>) => Record<string, unknown>)
// `search` 可以是查询参数对象,或是接收旧搜索参数返回新参数的函数。根据路由可能需要提供部分或全部查询参数。TypeScript 会提示必需搜索参数。
search:
| Record<string, unknown>
| ((prevSearch: Record<string, unknown>) => Record<string, unknown>)
// `hash` 可以是字符串,或是接收旧哈希值返回新值的函数。
hash?: string | ((prevHash: string) => string)
// `state` 可以是状态对象,或是接收旧状态返回新状态的函数。状态存储在 history API 中,适合传递不想永久保存在URL搜索参数中的数据。
state?:
| Record<string, any>
| ((prevState: Record<string, unknown>) => Record<string, unknown>)
}
🧠 每个路由对象都有 to 属性,可作为任何导航或路由匹配 API 的 to 值。这能帮助你避免使用纯字符串,转而使用类型安全的路由引用:
import { Route as aboutRoute } from './routes/about.tsx'
function Comp() {
return <Link to={aboutRoute.to}>关于</Link>
}
import { Route as aboutRoute } from './routes/about.tsx'
function Comp() {
return <Link to={aboutRoute.to}>关于</Link>
}
这是扩展自 ToOptions 的核心 NavigateOptions 接口,所有执行实际导航的 API 都会使用该接口:
export type NavigateOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = ToOptions<TRouteTree, TFrom, TTo> & {
// `replace` 决定导航是否替换当前历史记录而非新增记录
replace?: boolean
// `resetScroll` 决定导航提交后是否重置滚动位置至 0,0
resetScroll?: boolean
// `hashScrollIntoView` 决定是否将匹配哈希的ID元素滚动至视口
hashScrollIntoView?: boolean | ScrollIntoViewOptions
// `viewTransition` 决定是否及如何调用 document.startViewTransition()
viewTransition?: boolean | ViewTransitionOptions
// `ignoreBlocker` 决定是否忽略可能阻止导航的拦截器
ignoreBlocker?: boolean
// `reloadDocument` 决定是否触发整页加载而非SPA导航
reloadDocument?: boolean
// `href` 可替代 `to` 直接导航至完整构建的href(如外部链接)
href?: string
}
export type NavigateOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = ToOptions<TRouteTree, TFrom, TTo> & {
// `replace` 决定导航是否替换当前历史记录而非新增记录
replace?: boolean
// `resetScroll` 决定导航提交后是否重置滚动位置至 0,0
resetScroll?: boolean
// `hashScrollIntoView` 决定是否将匹配哈希的ID元素滚动至视口
hashScrollIntoView?: boolean | ScrollIntoViewOptions
// `viewTransition` 决定是否及如何调用 document.startViewTransition()
viewTransition?: boolean | ViewTransitionOptions
// `ignoreBlocker` 决定是否忽略可能阻止导航的拦截器
ignoreBlocker?: boolean
// `reloadDocument` 决定是否触发整页加载而非SPA导航
reloadDocument?: boolean
// `href` 可替代 `to` 直接导航至完整构建的href(如外部链接)
href?: string
}
任何实际 <a> 标签都会使用扩展自 NavigateOptions 的 LinkOptions 接口:
export type LinkOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = NavigateOptions<TRouteTree, TFrom, TTo> & {
// 标准锚点标签的target属性
target?: HTMLAnchorElement['target']
// 默认为 `{ exact: false, includeHash: false }`
activeOptions?: {
exact?: boolean
includeHash?: boolean
includeSearch?: boolean
explicitUndefined?: boolean
}
// 设置后会在悬停时预加载目标路由并缓存指定毫秒数
preload?: false | 'intent'
// 延迟意图预加载的毫秒数。若在延迟前取消意图,则取消预加载
preloadDelay?: number
// 为true时不渲染href属性
disabled?: boolean
}
export type LinkOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = NavigateOptions<TRouteTree, TFrom, TTo> & {
// 标准锚点标签的target属性
target?: HTMLAnchorElement['target']
// 默认为 `{ exact: false, includeHash: false }`
activeOptions?: {
exact?: boolean
includeHash?: boolean
includeSearch?: boolean
explicitUndefined?: boolean
}
// 设置后会在悬停时预加载目标路由并缓存指定毫秒数
preload?: false | 'intent'
// 延迟意图预加载的毫秒数。若在延迟前取消意图,则取消预加载
preloadDelay?: number
// 为true时不渲染href属性
disabled?: boolean
}
了解相对导航和所有接口后,我们来看看可用的各类导航 API:
⚠️ 这些 API 都不能替代服务端重定向。若需在应用挂载前立即重定向,请使用服务端重定向而非客户端导航。
Link 是应用内导航最常用的方式。它渲染真实的 <a> 标签,支持标准属性如 target 等。除 LinkOptions 接口外,还支持以下属性:
export type LinkProps<
TFrom extends RoutePaths<RegisteredRouter['routeTree']> | string = string,
TTo extends string = '',
> = LinkOptions<RegisteredRouter['routeTree'], TFrom, TTo> & {
// 返回链接激活状态额外属性的函数(样式会合并,类名会拼接)
activeProps?:
| FrameworkHTMLAnchorTagAttributes
| (() => FrameworkHTMLAnchorAttributes)
// 返回链接非激活状态额外属性的函数
inactiveProps?:
| FrameworkHTMLAnchorAttributes
| (() => FrameworkHTMLAnchorAttributes)
}
export type LinkProps<
TFrom extends RoutePaths<RegisteredRouter['routeTree']> | string = string,
TTo extends string = '',
> = LinkOptions<RegisteredRouter['routeTree'], TFrom, TTo> & {
// 返回链接激活状态额外属性的函数(样式会合并,类名会拼接)
activeProps?:
| FrameworkHTMLAnchorTagAttributes
| (() => FrameworkHTMLAnchorAttributes)
// 返回链接非激活状态额外属性的函数
inactiveProps?:
| FrameworkHTMLAnchorAttributes
| (() => FrameworkHTMLAnchorAttributes)
}
创建静态链接示例:
import { Link } from '@tanstack/solid-router'
const link = <Link to="/about">关于</Link>
import { Link } from '@tanstack/solid-router'
const link = <Link to="/about">关于</Link>
带动态段落的链接示例:
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
>
博客文章
</Link>
)
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
>
博客文章
</Link>
)
动态段落参数通常为 string 类型,但也可在路由选项中解析为其他类型,类型检查会在编译时进行。
默认所有链接都是绝对的。若要创建相对于当前路由的链接,需提供 from 路径:
const postIdRoute = createRoute({
path: '/blog/post/$postId',
})
const link = (
<Link from={postIdRoute.fullPath} to="../categories">
分类
</Link>
)
const postIdRoute = createRoute({
path: '/blog/post/$postId',
})
const link = (
<Link from={postIdRoute.fullPath} to="../categories">
分类
</Link>
)
通常建议使用 route.fullPath 作为 from 路径以保证重构时的可维护性。
通过搜索参数传递额外上下文:
const link = (
<Link
to="/search"
search={{
query: 'tanstack',
}}
>
搜索
</Link>
)
const link = (
<Link
to="/search"
search={{
query: 'tanstack',
}}
>
搜索
</Link>
)
更新单个搜索参数而不影响其他参数:
const link = (
<Link
to="."
search={(prev) => ({
...prev,
page: prev.page + 1,
})}
>
下一页
</Link>
)
const link = (
<Link
to="."
search={(prev) => ({
...prev,
page: prev.page + 1,
})}
>
下一页
</Link>
)
搜索参数是高度动态的状态管理机制,必须确保类型安全。后续章节将详细介绍验证和类型安全等特性。
跳转至页面特定区域的示例:
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
hash="section-1"
>
第一节
</Link>
)
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
hash="section-1"
>
第一节
</Link>
)
Link 组件支持 activeProps 和 inactiveProps 来分别设置不同状态的额外属性。除样式和类名外,这些属性会覆盖原始属性。
示例:
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
activeProps={{
style: {
fontWeight: 'bold',
},
}}
>
第一节
</Link>
)
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
activeProps={{
style: {
fontWeight: 'bold',
},
}}
>
第一节
</Link>
)
Link 组件在激活状态时会添加 data-status="active" 属性,便于通过数据属性进行样式控制。
activeOptions 属性提供多种确定链接激活状态的配置:
export interface ActiveOptions {
// 为true时要求完全匹配(不包含子路由)
exact?: boolean
// 为true时要求哈希匹配
includeHash?: boolean
// 为true时要求搜索参数包含匹配
includeSearch?: boolean
// 修改includeSearch行为:显式undefined的属性必须不存在于当前URL
explicitUndefined?: boolean
}
export interface ActiveOptions {
// 为true时要求完全匹配(不包含子路由)
exact?: boolean
// 为true时要求哈希匹配
includeHash?: boolean
// 为true时要求搜索参数包含匹配
includeSearch?: boolean
// 修改includeSearch行为:显式undefined的属性必须不存在于当前URL
explicitUndefined?: boolean
}
默认检查路径名前缀匹配,若提供搜索参数则要求包含匹配,默认不检查哈希。
例如在 /blog/post/my-first-blog-post 路由下,以下链接会激活:
const link1 = (
<Link to="/blog/post/$postId" params={{ postId: 'my-first-blog-post' }}>
博客文章
</Link>
)
const link2 = <Link to="/blog/post">博客文章</Link>
const link3 = <Link to="/blog">博客文章</Link>
const link1 = (
<Link to="/blog/post/$postId" params={{ postId: 'my-first-blog-post' }}>
博客文章
</Link>
)
const link2 = <Link to="/blog/post">博客文章</Link>
const link3 = <Link to="/blog">博客文章</Link>
而以下链接不会激活:
const link4 = (
<Link to="/blog/post/$postId" params={{ postId: 'my-second-blog-post' }}>
博客文章
</Link>
)
const link4 = (
<Link to="/blog/post/$postId" params={{ postId: 'my-second-blog-post' }}>
博客文章
</Link>
)
对于需要精确匹配的链接(如首页),可设置 exact: true:
const link = (
<Link to="/" activeOptions={{ exact: true }}>
首页
</Link>
)
```- 若希望在匹配时包含哈希值,可传入 `includeHash: true` 选项
- 若**不**希望在匹配时包含搜索参数,可传入 `includeSearch: false` 选项
### 向子元素传递 `isActive`
`Link` 组件接受子元素为函数的形式,允许你将 `isActive` 属性传递给子元素。例如,可以根据父级链接的激活状态来设置子组件的样式:
```tsx
const link = (
<Link to="/blog/post">
{({ isActive }) => {
return (
<>
<span>My Blog Post</span>
<icon className={isActive ? 'active' : 'inactive'} />
</>
)
}}
</Link>
)
const link = (
<Link to="/" activeOptions={{ exact: true }}>
首页
</Link>
)
```- 若希望在匹配时包含哈希值,可传入 `includeHash: true` 选项
- 若**不**希望在匹配时包含搜索参数,可传入 `includeSearch: false` 选项
### 向子元素传递 `isActive`
`Link` 组件接受子元素为函数的形式,允许你将 `isActive` 属性传递给子元素。例如,可以根据父级链接的激活状态来设置子组件的样式:
```tsx
const link = (
<Link to="/blog/post">
{({ isActive }) => {
return (
<>
<span>My Blog Post</span>
<icon className={isActive ? 'active' : 'inactive'} />
</>
)
}}
</Link>
)
Link 组件支持在用户意图(如悬停或触摸开始)时自动预加载路由。这可以通过路由器的默认配置(后续会详细介绍)或向 Link 组件传递 preload='intent' 属性来实现。示例如下:
const link = (
<Link to="/blog/post/$postId" preload="intent">
Blog Post
</Link>
)
const link = (
<Link to="/blog/post/$postId" preload="intent">
Blog Post
</Link>
)
启用预加载后,如果异步路由依赖加载较快,这一简单技巧可以显著提升应用的感知性能。
更棒的是,如果使用像 @tanstack/query 这样的缓存优先库,预加载的路由会被保留,当用户稍后导航到该路由时,可以直接使用缓存数据,同时后台重新验证(stale-while-revalidate)。
预加载还支持配置超时时间,决定用户悬停多久后触发基于意图的预加载。默认超时为 50 毫秒,但你可以通过向 Link 组件传递 preloadTimeout 属性来自定义等待时间(单位为毫秒):
const link = (
<Link to="/blog/post/$postId" preload="intent" preloadTimeout={100}>
Blog Post
</Link>
)
const link = (
<Link to="/blog/post/$postId" preload="intent" preloadTimeout={100}>
Blog Post
</Link>
)
⚠️ 由于 Link 组件内置了对 href、Cmd/Ctrl + 点击以及激活/非激活状态的支持,建议在用户可交互的场景(如链接、按钮)中使用 Link 组件而非 useNavigate。但在某些情况下(如异步操作成功后的导航),useNavigate 仍是必要的。
useNavigate 钩子返回一个可调用的 navigate 函数,用于命令式导航。它非常适合在副作用(如异步操作成功)中触发导航。示例如下:
function Component() {
const navigate = useNavigate({ from: '/posts/$postId' })
const handleSubmit = async (e: FrameworkFormEvent) => {
e.preventDefault()
const response = await fetch('/posts', {
method: 'POST',
body: JSON.stringify({ title: 'My First Post' }),
})
const { id: postId } = await response.json()
if (response.ok) {
navigate({ to: '/posts/$postId', params: { postId } })
}
}
}
function Component() {
const navigate = useNavigate({ from: '/posts/$postId' })
const handleSubmit = async (e: FrameworkFormEvent) => {
e.preventDefault()
const response = await fetch('/posts', {
method: 'POST',
body: JSON.stringify({ title: 'My First Post' }),
})
const { id: postId } = await response.json()
if (response.ok) {
navigate({ to: '/posts/$postId', params: { postId } })
}
}
}
🧠 如上所示,可通过 from 选项指定导航的起始路由。虽然也可以在每次调用 navigate 时传递该选项,但建议在钩子调用时传入,以减少潜在错误并简化类型声明!
useNavigate 返回的 navigate 函数接受 NavigateOptions 接口。
有时你可能需要在组件挂载时立即导航。虽然可以使用 useNavigate 配合副作用(如 useEffect),但更简单的方式是直接渲染 Navigate 组件:
function Component() {
return <Navigate to="/posts/$postId" params={{ postId: 'my-first-post' }} />
}
function Component() {
return <Navigate to="/posts/$postId" params={{ postId: 'my-first-post' }} />
}
Navigate 组件的作用是在组件挂载时立即导航到指定路由,非常适合处理纯客户端的重定向。但注意,它绝不能替代服务端处理的重定向逻辑。
router.navigate 方法与 useNavigate 返回的 navigate 函数功能相同,同样接受 NavigateOptions 接口。不同于 useNavigate 钩子,它可以在 router 实例可用的任何地方调用,因此非常适合在应用全局(包括框架外部)进行命令式导航。
useMatchRoute 钩子和 <MatchRoute> 组件功能相同,但钩子更灵活。它们都接受标准的导航 ToOptions 接口(作为选项或属性),并在路由匹配时返回 true/false。此外,pending 选项会在路由正在过渡时返回 true,这对展示乐观 UI 非常有用:
function Component() {
return (
<div>
<Link to="/users">
Users
<MatchRoute to="/users" pending>
<Spinner />
</MatchRoute>
</Link>
</div>
)
}
function Component() {
return (
<div>
<Link to="/users">
Users
<MatchRoute to="/users" pending>
<Spinner />
</MatchRoute>
</Link>
</div>
)
}
组件版 <MatchRoute> 还支持以函数形式渲染子元素:
function Component() {
return (
<div>
<Link to="/users">
Users
<MatchRoute to="/users" pending>
{(match) => {
return <Spinner show={match} />
}}
</MatchRoute>
</Link>
</div>
)
}
function Component() {
return (
<div>
<Link to="/users">
Users
<MatchRoute to="/users" pending>
{(match) => {
return <Spinner show={match} />
}}
</MatchRoute>
</Link>
</div>
)
}
钩子版 useMatchRoute 返回一个可编程调用的函数来检查路由匹配:
function Component() {
const matchRoute = useMatchRoute()
useEffect(() => {
if (matchRoute({ to: '/users', pending: true })) {
console.info('The /users route is matched and pending')
}
})
return (
<div>
<Link to="/users">Users</Link>
</div>
)
}
function Component() {
const matchRoute = useMatchRoute()
useEffect(() => {
if (matchRoute({ to: '/users', pending: true })) {
console.info('The /users route is matched and pending')
}
})
return (
<div>
<Link to="/users">Users</Link>
</div>
)
}
导航相关的知识就这么多!希望你现在对如何在应用中自如导航有了信心。接下来继续前进吧!
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.