信不信由你,应用程序内的每次导航都是相对的,即使您没有使用显式的相对路径语法 (../../somewhere)。任何时候点击链接或进行命令式导航调用,您将始终拥有一个起始路径和一个目标路径,这意味着您正在从一个路由导航到另一个路由。
TanStack Router 在每次导航时都牢记相对导航的这个恒定概念,因此您会经常在 API 中看到两个属性
⚠️ 如果未提供 from 路由路径,则路由器将假定您从根 / 路由导航,并且仅自动完成绝对路径。毕竟,您需要知道您从哪里来,才能知道您要去哪里 😉。
TanStack Router 中的每个导航和路由匹配 API 都使用相同的核心接口,只是根据 API 略有不同。这意味着您可以学习一次导航和路由匹配,并在整个库中使用相同的语法和概念。
这是核心的 ToOptions 接口,用于每个导航和路由匹配 API 中
type ToOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = {
// `from` is an optional route ID or path. If it is not supplied, only absolute paths will be auto-completed and type-safe. It's common to supply the route.fullPath of the origin route you are rendering from for convenience. If you don't know the origin route, leave this empty and work with absolute paths or unsafe relative paths.
from: string
// `to` can be an absolute route path or a relative path from the `from` option to a valid route path. ⚠️ Do not interpolate path params, hash or search params into the `to` options. Use the `params`, `search`, and `hash` options instead.
to: string
// `params` is either an object of path params to interpolate into the `to` option or a function that supplies the previous params and allows you to return new ones. This is the only way to interpolate dynamic parameters into the final URL. Depending on the `from` and `to` route, you may need to supply none, some or all of the path params. TypeScript will notify you of the required params if there are any.
params:
| Record<string, unknown>
| ((prevParams: Record<string, unknown>) => Record<string, unknown>)
// `search` is either an object of query params or a function that supplies the previous search and allows you to return new ones. Depending on the `from` and `to` route, you may need to supply none, some or all of the query params. TypeScript will notify you of the required search params if there are any.
search:
| Record<string, unknown>
| ((prevSearch: Record<string, unknown>) => Record<string, unknown>)
// `hash` is either a string or a function that supplies the previous hash and allows you to return a new one.
hash?: string | ((prevHash: string) => string)
// `state` is either an object of state or a function that supplies the previous state and allows you to return a new one. State is stored in the history API and can be useful for passing data between routes that you do not want to permanently store in URL search params.
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` is an optional route ID or path. If it is not supplied, only absolute paths will be auto-completed and type-safe. It's common to supply the route.fullPath of the origin route you are rendering from for convenience. If you don't know the origin route, leave this empty and work with absolute paths or unsafe relative paths.
from: string
// `to` can be an absolute route path or a relative path from the `from` option to a valid route path. ⚠️ Do not interpolate path params, hash or search params into the `to` options. Use the `params`, `search`, and `hash` options instead.
to: string
// `params` is either an object of path params to interpolate into the `to` option or a function that supplies the previous params and allows you to return new ones. This is the only way to interpolate dynamic parameters into the final URL. Depending on the `from` and `to` route, you may need to supply none, some or all of the path params. TypeScript will notify you of the required params if there are any.
params:
| Record<string, unknown>
| ((prevParams: Record<string, unknown>) => Record<string, unknown>)
// `search` is either an object of query params or a function that supplies the previous search and allows you to return new ones. Depending on the `from` and `to` route, you may need to supply none, some or all of the query params. TypeScript will notify you of the required search params if there are any.
search:
| Record<string, unknown>
| ((prevSearch: Record<string, unknown>) => Record<string, unknown>)
// `hash` is either a string or a function that supplies the previous hash and allows you to return a new one.
hash?: string | ((prevHash: string) => string)
// `state` is either an object of state or a function that supplies the previous state and allows you to return a new one. State is stored in the history API and can be useful for passing data between routes that you do not want to permanently store in URL search params.
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}>About</Link>
}
import { Route as aboutRoute } from './routes/about.tsx'
function Comp() {
return <Link to={aboutRoute.to}>About</Link>
}
这是核心的 NavigateOptions 接口,它扩展了 ToOptions。任何实际执行导航的 API 都将使用此接口
export type NavigateOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = ToOptions<TRouteTree, TFrom, TTo> & {
// `replace` is a boolean that determines whether the navigation should replace the current history entry or push a new one.
replace?: boolean
// `resetScroll` is a boolean that determines whether scroll position will be reset to 0,0 after the location is committed to browser history.
resetScroll?: boolean
// `hashScrollIntoView` is a boolean or object that determines whether an id matching the hash will be scrolled into view after the location is committed to history.
hashScrollIntoView?: boolean | ScrollIntoViewOptions
// `viewTransition` is either a boolean or function that determines if and how the browser will call document.startViewTransition() when navigating.
viewTransition?: boolean | ViewTransitionOptions
// `ignoreBlocker` is a boolean that determines if navigation should ignore any blockers that might prevent it.
ignoreBlocker?: boolean
// `reloadDocument` is a boolean that determines if navigation to a route inside of router will trigger a full page load instead of the traditional SPA navigation.
reloadDocument?: boolean
// `href` is a string that can be used in place of `to` to navigate to a full built href, e.g. pointing to an external target.
href?: string
}
export type NavigateOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = ToOptions<TRouteTree, TFrom, TTo> & {
// `replace` is a boolean that determines whether the navigation should replace the current history entry or push a new one.
replace?: boolean
// `resetScroll` is a boolean that determines whether scroll position will be reset to 0,0 after the location is committed to browser history.
resetScroll?: boolean
// `hashScrollIntoView` is a boolean or object that determines whether an id matching the hash will be scrolled into view after the location is committed to history.
hashScrollIntoView?: boolean | ScrollIntoViewOptions
// `viewTransition` is either a boolean or function that determines if and how the browser will call document.startViewTransition() when navigating.
viewTransition?: boolean | ViewTransitionOptions
// `ignoreBlocker` is a boolean that determines if navigation should ignore any blockers that might prevent it.
ignoreBlocker?: boolean
// `reloadDocument` is a boolean that determines if navigation to a route inside of router will trigger a full page load instead of the traditional SPA navigation.
reloadDocument?: boolean
// `href` is a string that can be used in place of `to` to navigate to a full built href, e.g. pointing to an external target.
href?: string
}
任何实际的 <a> 标签都将提供 LinkOptions 接口,它扩展了 NavigateOptions
export type LinkOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = NavigateOptions<TRouteTree, TFrom, TTo> & {
// The standard anchor tag target attribute
target?: HTMLAnchorElement['target']
// Defaults to `{ exact: false, includeHash: false }`
activeOptions?: {
exact?: boolean
includeHash?: boolean
includeSearch?: boolean
explicitUndefined?: boolean
}
// If set, will preload the linked route on hover and cache it for this many milliseconds in hopes that the user will eventually navigate there.
preload?: false | 'intent'
// Delay intent preloading by this many milliseconds. If the intent exits before this delay, the preload will be cancelled.
preloadDelay?: number
// If true, will render the link without the href attribute
disabled?: boolean
}
export type LinkOptions<
TRouteTree extends AnyRoute = AnyRoute,
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
> = NavigateOptions<TRouteTree, TFrom, TTo> & {
// The standard anchor tag target attribute
target?: HTMLAnchorElement['target']
// Defaults to `{ exact: false, includeHash: false }`
activeOptions?: {
exact?: boolean
includeHash?: boolean
includeSearch?: boolean
explicitUndefined?: boolean
}
// If set, will preload the linked route on hover and cache it for this many milliseconds in hopes that the user will eventually navigate there.
preload?: false | 'intent'
// Delay intent preloading by this many milliseconds. If the intent exits before this delay, the preload will be cancelled.
preloadDelay?: number
// If true, will render the link without the href attribute
disabled?: boolean
}
了解了相对导航和所有接口之后,让我们讨论一下您可以使用的不同风格的导航 API
⚠️ 这些 API 都不能替代服务器端重定向。如果您需要在挂载应用程序之前立即将用户从一个路由重定向到另一个路由,请使用服务器端重定向而不是客户端导航。
Link 组件是在应用程序内导航的最常见方式。它渲染一个实际的 <a> 标签,其中包含有效的 href 属性,可以单击甚至 cmd/ctrl + 单击以在新标签页中打开。它还支持任何普通的 <a> 属性,包括 target 以在新窗口中打开链接等。
除了 LinkOptions 接口外,Link 组件还支持以下 props
export type LinkProps<
TFrom extends RoutePaths<RegisteredRouter['routeTree']> | string = string,
TTo extends string = '',
> = LinkOptions<RegisteredRouter['routeTree'], TFrom, TTo> & {
// A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
activeProps?:
| FrameworkHTMLAnchorTagAttributes
| (() => FrameworkHTMLAnchorAttributes)
// A function that returns additional props for the `inactive` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
inactiveProps?:
| FrameworkHTMLAnchorAttributes
| (() => FrameworkHTMLAnchorAttributes)
}
export type LinkProps<
TFrom extends RoutePaths<RegisteredRouter['routeTree']> | string = string,
TTo extends string = '',
> = LinkOptions<RegisteredRouter['routeTree'], TFrom, TTo> & {
// A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
activeProps?:
| FrameworkHTMLAnchorTagAttributes
| (() => FrameworkHTMLAnchorAttributes)
// A function that returns additional props for the `inactive` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
inactiveProps?:
| FrameworkHTMLAnchorAttributes
| (() => FrameworkHTMLAnchorAttributes)
}
让我们创建一个简单的静态链接!
import { Link } from '@tanstack/react-router'
const link = <Link to="/about">About</Link>
import { Link } from '@tanstack/react-router'
const link = <Link to="/about">About</Link>
动态链接是指在其路径中包含动态段的链接。例如,指向博客文章的链接可能如下所示
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
>
Blog Post
</Link>
)
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
>
Blog Post
</Link>
)
请记住,通常动态段参数是 string 值,但它们也可以是您在路由选项中将其解析为的任何其他类型。无论哪种方式,类型都将在编译时进行检查,以确保您传递的类型正确。
默认情况下,除非提供 from 路由路径,否则所有链接都是绝对链接。这意味着无论您当前在哪个路由上,上面的链接都将始终导航到 /about 路由。
如果您想创建一个相对于当前路由的链接,您可以提供 from 路由路径
const postIdRoute = createRoute({
path: '/blog/post/$postId',
})
const link = (
<Link from={postIdRoute.fullPath} to="../categories">
Categories
</Link>
)
const postIdRoute = createRoute({
path: '/blog/post/$postId',
})
const link = (
<Link from={postIdRoute.fullPath} to="../categories">
Categories
</Link>
)
如上所示,通常提供 route.fullPath 作为 from 路由路径。这是因为 route.fullPath 是一个引用,如果您重构应用程序,它将会更新。但是,有时不可能直接导入路由,在这种情况下,将路由路径直接作为字符串提供是可以的。它仍然会像往常一样进行类型检查!
搜索参数是为路由提供额外上下文的好方法。例如,您可能想为搜索页面提供搜索查询
const link = (
<Link
to="/search"
search={{
query: 'tanstack',
}}
>
Search
</Link>
)
const link = (
<Link
to="/search"
search={{
query: 'tanstack',
}}
>
Search
</Link>
)
通常也希望更新单个搜索参数,而无需提供有关现有路由的任何其他信息。例如,您可能想更新搜索结果的页码
const link = (
<Link
to="."
search={(prev) => ({
...prev,
page: prev.page + 1,
})}
>
Next Page
</Link>
)
const link = (
<Link
to="."
search={(prev) => ({
...prev,
page: prev.page + 1,
})}
>
Next Page
</Link>
)
搜索参数是一种高度动态的状态管理机制,因此务必确保您将正确的类型传递给搜索参数。我们将在后面的章节中详细了解如何验证和确保搜索参数类型安全以及其他出色功能!
哈希链接是链接到页面特定部分的好方法。例如,您可能想链接到博客文章的特定部分
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
hash="section-1"
>
Section 1
</Link>
)
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
hash="section-1"
>
Section 1
</Link>
)
Link 组件支持两个额外的 props:activeProps 和 inactiveProps。这些 props 是返回链接的 active 和 inactive 状态的附加 props 的函数。此处传递的所有 props(样式和类除外)都将覆盖传递给 Link 的原始 props。传递的任何样式或类都会合并在一起。
这是一个例子
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
activeProps={{
style: {
fontWeight: 'bold',
},
}}
>
Section 1
</Link>
)
const link = (
<Link
to="/blog/post/$postId"
params={{
postId: 'my-first-blog-post',
}}
activeProps={{
style: {
fontWeight: 'bold',
},
}}
>
Section 1
</Link>
)
除了 activeProps 和 inactiveProps props 外,当 Link 组件处于活动状态时,还会向渲染的元素添加 data-status 属性。此属性将为 active 或 undefined,具体取决于链接的当前状态。如果您喜欢使用 data 属性来设置链接样式而不是 props,这将非常方便。
Link 组件带有一个 activeOptions 属性,该属性提供了一些选项来确定链接是否处于活动状态。以下接口描述了这些选项
export interface ActiveOptions {
// If true, the link will be active if the current route matches the `to` route path exactly (no children routes)
// Defaults to `false`
exact?: boolean
// If true, the link will only be active if the current URL hash matches the `hash` prop
// Defaults to `false`
includeHash?: boolean // Defaults to false
// If true, the link will only be active if the current URL search params inclusively match the `search` prop
// Defaults to `true`
includeSearch?: boolean
// This modifies the `includeSearch` behavior.
// If true, properties in `search` that are explicitly `undefined` must NOT be present in the current URL search params for the link to be active.
// defaults to `false`
explicitUndefined?: boolean
}
export interface ActiveOptions {
// If true, the link will be active if the current route matches the `to` route path exactly (no children routes)
// Defaults to `false`
exact?: boolean
// If true, the link will only be active if the current URL hash matches the `hash` prop
// Defaults to `false`
includeHash?: boolean // Defaults to false
// If true, the link will only be active if the current URL search params inclusively match the `search` prop
// Defaults to `true`
includeSearch?: boolean
// This modifies the `includeSearch` behavior.
// If true, properties in `search` that are explicitly `undefined` must NOT be present in the current URL search params for the link to be active.
// defaults to `false`
explicitUndefined?: boolean
}
默认情况下,它将检查生成的pathname 是否是当前路由的前缀。如果提供了任何搜索参数,它将检查它们是否包含性地匹配当前位置中的参数。默认情况下不检查哈希。
例如,如果您在 /blog/post/my-first-blog-post 路由上,则以下链接将处于活动状态
const link1 = (
<Link to="/blog/post/$postId" params={{ postId: 'my-first-blog-post' }}>
Blog Post
</Link>
)
const link2 = <Link to="/blog/post">Blog Post</Link>
const link3 = <Link to="/blog">Blog Post</Link>
const link1 = (
<Link to="/blog/post/$postId" params={{ postId: 'my-first-blog-post' }}>
Blog Post
</Link>
)
const link2 = <Link to="/blog/post">Blog Post</Link>
const link3 = <Link to="/blog">Blog Post</Link>
但是,以下链接将不会处于活动状态
const link4 = (
<Link to="/blog/post/$postId" params={{ postId: 'my-second-blog-post' }}>
Blog Post
</Link>
)
const link4 = (
<Link to="/blog/post/$postId" params={{ postId: 'my-second-blog-post' }}>
Blog Post
</Link>
)
对于某些链接,仅当它们完全匹配时才处于活动状态是很常见的。主页链接就是一个很好的例子。在这样的情况下,您可以传递 exact: true 选项
const link = (
<Link to="/" activeOptions={{ exact: true }}>
Home
</Link>
)
const link = (
<Link to="/" activeOptions={{ exact: true }}>
Home
</Link>
)
这将确保当您是子路由时,链接不会处于活动状态。
还有一些需要注意的选项
Link 组件接受一个函数作为其 children,允许您将其 isActive 属性传播给子组件。例如,您可以根据父链接是否处于活动状态来设置子组件的样式
const link = (
<Link to="/blog/post">
{({ isActive }) => {
return (
<>
<span>My Blog Post</span>
<icon className={isActive ? 'active' : 'inactive'} />
</>
)
}}
</Link>
)
const link = (
<Link to="/blog/post">
{({ isActive }) => {
return (
<>
<span>My Blog Post</span>
<icon className={isActive ? 'active' : 'inactive'} />
</>
)
}}
</Link>
)
Link 组件支持在有意图时自动预加载路由(目前为悬停或 touchstart)。这可以在路由器选项中配置为默认值(我们稍后会详细讨论),或者通过将 preload='intent' prop 传递给 Link 组件来实现。这是一个例子
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 毫秒,但您可以通过将 preloadTimeout prop 传递给 Link 组件并设置您希望等待的毫秒数来更改此超时时间
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 + 点击能力以及 active/inactive 功能的内置特性,建议对用户可以交互的任何内容(例如链接、按钮)使用 Link 组件,而不是 useNavigate。但是,在某些情况下,需要使用 useNavigate 来处理副作用导航(例如,成功的异步操作导致导航)。
useNavigate hook 返回一个 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 选项以在 hook 调用中指定要从中导航的路由。虽然也可以在每次调用结果 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 hook 不同,它在 router 实例可用的任何地方都可用,因此是从应用程序中的任何地方(包括框架外部)以命令式方式导航的好方法。
useMatchRoute hook 和 <MatchRoute> 组件是相同的,但 hook 更灵活一些。它们都接受标准的导航 ToOptions 接口,既可以作为选项也可以作为 props,并返回 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> 也可以与函数一起用作 children,以便在路由匹配时渲染某些内容
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>
)
}
hook 版本 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>
)
}
呼!导航内容真多!也就是说,希望您现在对在您的应用程序中导航感觉良好。让我们继续前进!
您每周的 JavaScript 新闻。每周一免费发送给超过 100,000 名开发人员。