首页 / TanStack 生态入门教程 / 类型安全导航与 Link

TanStack 生态入门教程

类型安全导航与 Link

本教程共 38 篇 · 第 18 篇 · 更新于 2026-07-27 · 约 14 分钟阅读

TanStackTanStack 生态入门教程TanStack RouterLinkuseNavigate类型安全导航navigation blockingactive link

18. 类型安全导航与 Link

本节目标:学会用 <Link> 组件、useNavigate 钩子、<Navigate> 组件、router.navigate 四种导航方式,搞懂类型安全的路径参数和搜索参数怎么传,学会用 useBlocker 阻塞导航保护未保存数据,还有 active link 高亮。学完你能在应用里自如地跳转页面、传参、防误操作。

18.1 导航的本质:从哪来,到哪去

TanStack Router 的导航 API 有个核心思想:所有导航都是相对的

不管你写的是绝对路径 /about 还是相对路径 ../categories,每次导航都有两个要素:

  • from:从哪个路由出发(起点)
  • to:要到哪个路由去(终点)

哪怕你写 <Link to="/about"> 没显式写 from,Router 也会默认 from 是当前路由。理解这一点,后面的 fromtoparamssearch 选项就好懂了。

Note

如果不传 from,Router 假设你从根 / 出发,只对绝对路径做类型补全。要享受相对路径的类型安全,最好传 from,通常用 route.fullPath(重构时能跟着变)。

18.2 四种导航方式

TanStack Router 提供四种导航 API,适用场景不同。

方式用途适合场景
<Link> 组件渲染可点击的 <a> 标签用户主动点击的导航
useNavigate() 钩子命令式导航副作用触发的导航(如提交成功后跳转)
<Navigate> 组件组件挂载即跳转客户端重定向
router.navigate()在任意地方命令式导航React 树之外(如工具函数)

它们都共用一套核心选项接口(ToOptions),学一遍到处用。

<Link> 是最常用的导航方式。它渲染一个真实的 <a> 标签,有 href 属性,能被搜索引擎抓取,支持 cmd/ctrl + 点击在新标签打开。

18.3.1 基础链接

import { Link } from '@tanstack/react-router'

function Nav() {
  return <Link to="/about">关于</Link>
}

to 写绝对路径,TypeScript 会检查 /about 这个路由是否存在。写错一个字母(比如 /abot)直接红线报错。这就是类型安全导航的核心体验。

18.3.2 动态参数链接

链接到动态路由,用 params 传参数:

<Link
  to="/blog/post/$postId"
  params={{ postId: 'my-first-post' }}
>
  第一篇文章
</Link>

$postId 这种动态段,必须通过 params 传值,不能直接拼到 to 字符串里to="/blog/post/my-first-post" 这种写法类型检查会失败,因为类型系统不知道你要匹配哪个动态路由。

Warning

别把路径参数、搜索参数、hash 拼进 to 字符串。to 只写路由模板,参数用 paramssearchhash 选项传。拼字符串会破坏类型安全。

18.3.3 相对路径链接

from 后能用相对路径:

const postIdRoute = createRoute({
  path: '/blog/post/$postId',
})

<Link from={postIdRoute.fullPath} to="../categories">
  分类
</Link>

from 是起点,to 是相对起点的路径。../categories 表示从当前路由往上一级,再进 categories。用 route.fullPath 而不是字符串字面量,重构时路径变了能跟着更新。

18.3.4 特殊相对路径 ...

两个特殊相对路径很常用:

  • to=".":刷新当前路由(重新跑 loader)
  • to="..":回到父路由
function PostComponent() {
  return (
    <div>
      {/* 刷新当前路由 /posts/$postId */}
      <Link to=".">刷新本页</Link>
      {/* 回到 /posts */}
      <Link to="..">返回列表</Link>
    </div>
  )
}

to="." 适合”重新加载当前数据”的场景,to=".." 适合”返回上级”的面包屑导航。

18.3.5 搜索参数链接

search 传搜索参数:

<Link to="/search" search={{ query: 'tanstack' }}>
  搜索 tanstack
</Link>

更新单个搜索参数(保留其他),用函数形式:

<Link
  to="."
  search={(prev) => ({
    ...prev,
    page: prev.page + 1,
  })}
>
  下一页
</Link>

search 接收函数时,参数 prev 是当前搜索参数,返回新的搜索参数。...prev 展开保留其他参数,只改 page。这是分页导航的标准写法。

Tip

搜索参数也是类型安全的。第 19 章会讲怎么用 validateSearch 给搜索参数加 Schema 校验,传错类型直接报错。

18.3.6 Hash 链接

链接到页面某段,用 hash

<Link
  to="/blog/post/$postId"
  params={{ postId: 'my-first-post' }}
  hash="section-1"
>
  第一节
</Link>
Warning

Hash 只在客户端可用,浏览器不会把 hash 发给服务器。SSR 场景下用 hash 渲染内容会导致 hydration 不匹配,慎用。

18.3.7 可选参数导航

可选路径参数用 {-$paramName} 语法,导航时有三种玩法:

{/* 带参数 */}
<Link to="/posts/{-$category}" params={{ category: 'tech' }}>
  技术文章
</Link>

{/* 不带参数(设为 undefined) */}
<Link to="/posts/{-$category}" params={{ category: undefined }}>
  全部文章
</Link>

{/* 继承当前参数(传空对象) */}
<Link to="/posts/{-$category}" params={{}}>
  当前分类
</Link>

设为 undefined 是显式移除参数,传空对象 {} 是继承当前所有参数。函数形式也能用:

<Link
  to="/posts/{-$category}"
  params={(prev) => ({ ...prev, category: 'news' })}
>
  新闻分类
</Link>

18.4 Active Link:高亮当前页

<Link> 能自动判断当前路由是否匹配,匹配时加 active 状态。配合 activePropsdata-status 属性,能轻松实现导航高亮。

18.4.1 activeProps 和 inactiveProps

<Link
  to="/blog/post/$postId"
  params={{ postId: 'my-first-post' }}
  activeProps={{ style: { fontWeight: 'bold' } }}
  inactiveProps={{ style: { color: 'gray' } }}
>
  第一篇文章
</Link>

匹配时 activeProps 生效(加粗),不匹配时 inactiveProps 生效(变灰)。样式会合并,className 会拼接。

18.4.2 data-status 属性

<Link> 渲染的 <a> 标签会带 data-status="active" 属性(不活跃时没有这个属性)。用 CSS 选择器也能高亮:

a[data-status='active'] {
  font-weight: bold;
  color: blue;
}

18.4.3 activeOptions 精细控制

默认情况下,<Link> 匹配规则是”当前路径是 to 路径的前缀”。比如在 /blog/post/my-first-post<Link to="/blog"> 也算 active。但这种行为有时不对,比如首页链接。

activeOptions 精细控制:

<Link to="/" activeOptions={{ exact: true }}>
  首页
</Link>

exact: true 表示必须完全匹配才算 active。在 /blog/post/123 时,首页链接不会高亮。

完整选项:

  • exact:是否完全匹配(默认 false
  • includeHash:是否检查 hash 匹配(默认 false
  • includeSearch:是否检查搜索参数匹配(默认 true
  • explicitUndefined:搜索参数里显式 undefined 的字段必须不存在才算匹配(默认 false

18.4.4 把 isActive 传给子组件

<Link>children 能是函数,接收 { isActive }

<Link to="/blog/post">
  {({ isActive }) => (
    <>
      <span>我的文章</span>
      <icon className={isActive ? 'active' : 'inactive'} />
    </>
  )}
</Link>

子组件能根据 isActive 切换样式,比 activeProps 更灵活。

<Link> 支持鼠标悬停时预加载目标路由,提升感知性能:

<Link to="/blog/post/$postId" params={{ postId: '123' }} preload="intent">
  文章 123
</Link>

preload="intent" 表示用户悬停(或触摸开始)时预加载。配合 preloadDelay 控制延迟(默认 50ms),避免鼠标快速划过时疯狂预加载:

<Link to="/blog/post/$postId" preload="intent" preloadDelay={100}>
  文章 123
</Link>
Tip

预取也能在 Router 全局配置:createRouter({ routeTree, defaultPreload: 'intent' })。这样所有 <Link> 默认都预取,不用每个都写。

18.6 useNavigate:命令式导航

用户能点的导航用 <Link>,副作用触发的导航用 useNavigate。比如表单提交成功后跳转:

import { useNavigate } from '@tanstack/react-router'

function PostForm() {
  const navigate = useNavigate({ from: '/posts/$postId' })

  const handleSubmit = async (e) => {
    e.preventDefault()
    const response = await fetch('/posts', { method: 'POST', ... })
    const { id: postId } = await response.json()

    if (response.ok) {
      navigate({ to: '/posts/$postId', params: { postId } })
    }
  }

  return <form onSubmit={handleSubmit}>...</form>
}

useNavigate({ from: ... }) 在钩子里指定 from,省得每次 navigate 都写。返回的 navigate 函数接收 NavigateOptions,和 <Link> 的选项基本一致(多了 replaceresetScroll 等)。

Note

能用 <Link> 就别用 useNavigate<Link> 渲染真实 <a> 标签,有 href、能 cmd+点击新标签打开、对 SEO 友好。useNavigate 只在副作用导航(如表单提交、登录成功)时用。

18.7 <Navigate> 组件和 router.navigate

18.7.1 <Navigate> 组件

组件挂载时立即跳转,相当于客户端重定向:

import { Navigate } from '@tanstack/react-router'

function OldPostRedirect({ postId }) {
  return <Navigate to="/posts/$postId" params={{ postId }} />
}

不用 useEffect + useNavigate,直接渲染 <Navigate> 就行。但它不能替代服务端重定向—SSR 场景下的重定向要在服务端做。

18.7.2 router.navigate

在 React 树之外(如工具函数、定时器回调)需要导航,用 router.navigate

import router from './router'

function logout() {
  localStorage.removeItem('token')
  router.navigate({ to: '/login' })
}

router 实例到处都能 import,router.navigate 的接口和 useNavigate 返回的函数一样。适合非 React 代码(如 axios 拦截器里 401 时跳登录页)。

18.8 用 linkOptions 复用导航配置

导航配置多了会重复。比如多个地方都跳 /dashboard 带相同的 search,每次都写一遍麻烦还容易错。

linkOptions 函数能把配置抽出来,提前做类型检查:

import { linkOptions } from '@tanstack/react-router'

const dashboardLinkOptions = linkOptions({
  to: '/dashboard',
  search: { search: '' },
})

// 到处复用
function Nav() {
  return <Link {...dashboardLinkOptions}>仪表盘</Link>
}

// 也能给 navigate 用
const navigate = useNavigate()
navigate(dashboardLinkOptions)

// 还能给 redirect 用
beforeLoad: () => {
  throw redirect(dashboardLinkOptions)
}

linkOptions 接收对象或数组,提前类型检查,避免运行时才发现配置写错。数组形式适合批量定义导航栏配置:

const navItems = linkOptions([
  { to: '/dashboard', label: '概览', activeOptions: { exact: true } },
  { to: '/dashboard/invoices', label: '发票' },
  { to: '/dashboard/users', label: '用户' },
])

function Nav() {
  return navItems.map((item) => (
    <Link key={item.to} {...item} activeProps={{ className: 'font-bold' }}>
      {item.label}
    </Link>
  ))
}

label 这种自定义字段也能放进 linkOptions,类型不会丢。

<Link> 默认渲染原生 <a> 标签。想用第三方 UI 库的链接组件(Chakra UI、MUI、Mantine),同时保留 TanStack Router 的类型安全,用 createLink

18.9.1 基础自定义

import * as React from 'react'
import { createLink, LinkComponent } from '@tanstack/react-router'

interface BasicLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  // 自定义 props
}

const BasicLinkComponent = React.forwardRef<HTMLAnchorElement, BasicLinkProps>(
  (props, ref) => (
    <a ref={ref} {...props} className="block px-3 py-2 text-blue-700" />
  ),
)

const CreatedLinkComponent = createLink(BasicLinkComponent)

export const CustomLink: LinkComponent<typeof BasicLinkComponent> = (props) => {
  return <CreatedLinkComponent preload="intent" {...props} />
}

createLink(YourComponent) 把你的组件包装成带类型安全的 Link。用法和原生 <Link> 一样:

<CustomLink to="/dashboard/invoices/$invoiceId" params={{ invoiceId: 0 }} />

18.9.2 配合第三方 UI 库

以 MUI 为例:

import { createLink } from '@tanstack/react-router'
import { Link } from '@mui/material'

export const CustomLink = createLink(Link)

简单包装就能用。要自定义就写个中间组件:

import React from 'react'
import { createLink, LinkComponent } from '@tanstack/react-router'
import { Link } from '@mui/material'
import type { LinkProps } from '@mui/material'

interface MUILinkProps extends LinkProps {
  // 自定义 props
}

const MUILinkComponent = React.forwardRef<HTMLAnchorElement, MUILinkProps>(
  (props, ref) => <Link ref={ref} {...props} />,
)

const CreatedLinkComponent = createLink(MUILinkComponent)

export const CustomLink: LinkComponent<typeof MUILinkComponent> = (props) => (
  <CreatedLinkComponent preload="intent" {...props} />
)

这样 <CustomLink to="/posts/123" params={{...}} /> 既能享受 TanStack Router 的类型检查,又能用 MUI 的样式。React Aria、Chakra UI、Mantine 都是这个套路。

18.10 navigation blocking:阻止误跳转

用户填了一半表单没保存,不小心点了导航链接,数据就丢了。useBlocker 能拦截这种导航,弹窗确认。

18.10.1 基础阻塞

import { useBlocker } from '@tanstack/react-router'
import { useState } from 'react'

function EditForm() {
  const [formIsDirty, setFormIsDirty] = useState(false)

  useBlocker({
    shouldBlockFn: () => {
      if (!formIsDirty) return false // 表单没改不阻塞
      const shouldLeave = confirm('表单没保存,确定离开吗?')
      return !shouldLeave // 用户取消就阻塞
    },
  })

  return <form>...</form>
}

shouldBlockFn 返回 true 阻塞导航,返回 false 放行。confirm 是浏览器原生确认框,够用。

18.10.2 带类型的阻塞条件

shouldBlockFn 接收 { current, next },类型安全地访问当前位置和目标位置:

useBlocker({
  shouldBlockFn: ({ current, next }) => {
    return (
      current.routeId === '/foo' &&
      next.fullPath === '/bar/$id' &&
      next.params.id === 123 &&
      next.search.hello === 'world'
    )
  },
  withResolver: true,
})

这种精确条件阻塞适合”只在特定跳转时拦截”。

18.10.3 自定义 UI

confirm 太丑,想用自定义弹窗,用 withResolver: true

const { proceed, reset, status } = useBlocker({
  shouldBlockFn: () => formIsDirty,
  withResolver: true,
})

return (
  <>
    <form>...</form>
    {status === 'blocked' && (
      <div className="modal">
        <p>表单没保存,确定离开吗?</p>
        <button onClick={proceed}>离开</button>
        <button onClick={reset}>留下</button>
      </div>
    )}
  </>
)

status'idle' | 'blocked' | 'proceeding' | 'resetting'proceed() 放行导航,reset() 取消导航。自定义 UI 完全由你控制。

18.10.4 处理浏览器关闭/刷新

useBlocker 默认不管浏览器关闭和刷新。要拦截这些,加 enableBeforeUnload

useBlocker({
  shouldBlockFn: () => { /* ... */ },
  enableBeforeUnload: formIsDirty, // 表单脏时注册 beforeunload
})

enableBeforeUnload 是布尔值或函数,为 true 时注册浏览器的 beforeunload 事件,用户关闭/刷新会弹浏览器原生确认框。

Warning

beforeunload 只能用浏览器原生确认框,没法自定义 UI。这是浏览器限制,不是 TanStack Router 的问题。

18.10.5 <Block> 组件

不喜欢钩子,用 <Block> 组件也行:

<Block shouldBlockFn={() => formIsDirty} withResolver>
  {({ status, proceed, reset }) => (
    <>
      {/* ... */}
      {status === 'blocked' && (
        <div>
          <button onClick={proceed}>离开</button>
          <button onClick={reset}>留下</button>
        </div>
      )}
    </>
  )}
</Block>

useBlocker 等价,看个人喜好。

18.11 MatchRoute:条件渲染

useMatchRoute 钩子和 <MatchRoute> 组件能判断某个路由当前是否匹配,配合 pending 选项做乐观 UI:

import { Link, MatchRoute } from '@tanstack/react-router'

function Nav() {
  return (
    <Link to="/users">
      用户
      <MatchRoute to="/users" pending>
        <Spinner />
      </MatchRoute>
    </Link>
  )
}

pending 表示”正在跳转到这个路由”时也匹配。用户点击链接后,链接旁边立即显示加载动画,等路由切换完成。

函数式用法:

<MatchRoute to="/users" pending>
  {(match) => <Spinner show={match} />}
</MatchRoute>

match 是布尔值,匹配为 true

18.12 常见坑

坑一:把参数拼进 to 字符串。 to="/posts/123" 类型检查失败。要写 to="/posts/$postId" params={{ postId: '123' }}

坑二:忘了 from 导致相对路径失效。 不传 from,相对路径 to="../categories" 类型推导不出,会回退成宽松类型。相对路径一定要配 from

坑三:active 高亮范围不对。 默认是前缀匹配,<Link to="/"> 在任何页面都算 active。首页链接加 activeOptions={{ exact: true }}

坑四:useNavigate 滥用。 能用 <Link> 的地方别用 useNavigate<Link> 渲染真实 <a> 标签,可右键新标签、可被爬虫抓取,体验和 SEO 都更好。

坑五:阻塞器忘了 enableBeforeUnload 只用 useBlocker 不加 enableBeforeUnload,用户关闭标签页不会提示,未保存数据照样丢。

坑六:hash 渲染导致 SSR 不匹配。 SSR 场景下用 hash 渲染内容、设 active,服务端拿不到 hash,会 hydration 报错。hash 只用于客户端定位。

坑七:linkOptions 不做类型检查。 直接写对象字面量 const opts = { to: '/dashboard' }to 被推成 string,类型检查失效。用 linkOptions({...}) 包一层。

18.13 小结

导航全家桶过了一遍:

  • 四种导航方式<Link>useNavigate<Navigate>router.navigate,按场景选。
  • 类型安全参数:路径参数用 params,搜索参数用 search,hash 用 hash,都别拼进 to
  • Active linkactivePropsdata-statusactiveOptions 三种高亮方式。
  • 预取preload="intent" 悬停预加载,全局配 defaultPreload
  • linkOptions:复用导航配置,提前类型检查。
  • createLink:包装第三方 UI 库的 Link,保留类型安全。
  • useBlocker:阻塞导航保护未保存数据,配合 withResolver 做自定义 UI,enableBeforeUnload 拦截浏览器关闭。

TanStack Router 的导航 API 最大的特点就是类型安全:写错路由名、漏传参数、传错类型,编译时全报错。这一章的 API 多,但核心就一个思想—让导航从”运行时才发现错”变成”编译时就知道错”。下一章讲搜索参数校验,把 URL 的查询字符串也纳入类型安全体系。