mcpField
N
navigation

navigation

github.com/tanstack开发15.3k201 次安装4 天前更新
代码仓库

关于 navigation

面向 React 应用的 TanStack Router 导航技能,介绍类型安全的路由实现。内容涵盖链接、编程式导航、预加载、导航拦截和滚动恢复。

主要功能

  1. 01类型安全的链接与路由参数
  2. 02链接激活状态
  3. 03相对导航与编程式导航
  4. 04路由预加载策略
  5. 05导航拦截与滚动恢复

适用场景

  1. 01构建带动态路由参数的链接
  2. 02表单提交后执行导航
  3. 03提前预加载待访问的路由
  4. 04离开前提示未保存的更改

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tanstack/router --skill navigation

安装后在 Claude Code 中输入/navigation确认已加载。

详细介绍

导航

设置

使用 to 和 params 的基础类型安全 Link:

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

function PostLink({ postId }: { postId: string }) {
  return (
    <Link to="/posts/$postId" params={{ postId }}>
      View Post
    </Link>
  )
}

核心模式

带活动状态的 Link
import { Link } from '@tanstack/react-router'

function NavLink() {
  return (
    <Link
      to="/posts"
      activeProps={{ className: 'font-bold' }}
      inactiveProps={{ className: 'text-gray-500' }}
      activeOptions={{ exact: true }}
    >
      Posts
    </Link>
  )
}

活动链接还会设置 data-status 属性为 "active",以便基于 CSS 设置样式。

activeOptions 用于控制匹配行为:

  • exact(默认为 false)——为 true 时,仅匹配完全相同的路径(不匹配子路径)
  • includeHash(默认为 false)——在活动状态匹配中包含哈希
  • includeSearch(默认为 true)——在活动状态匹配中包含搜索参数

子元素可以通过渲染函数接收 isActive:

<Link to="/posts">
  {({ isActive }) => <span className={isActive ? 'font-bold' : ''}>Posts</span>}
</Link>
使用 from 进行相对导航

如果不指定 from,导航会从根路径 / 解析。若要使用 .. 等相对路径,请提供 from:

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

export const Route = createFileRoute('/posts/$postId')({
  component: PostComponent,
})

function PostComponent() {
  return (
    <div>
      {/* Relative to current route */}
      <Link from={Route.fullPath} to="..">
        Back to Posts
      </Link>

      {/* "." reloads the current route */}
      <Link from={Route.fullPath} to=".">
        Reload
      </Link>
    </div>
  )
}
使用 useNavigate 进行编程式导航

仅在由副作用触发导航时(例如,表单提交后)使用 useNavigate。对于用户点击的内容,请优先使用 Link。

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

function CreatePostForm() {
  const navigate = useNavigate({ from: '/posts' })

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

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

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

Navigate 组件会在挂载时立即执行客户端导航:

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

function LegacyRedirect() {
  return <Navigate to="/posts/$postId" params={{ postId: 'my-first-post' }} />
}

只要能访问路由器实例,就可以在任何位置使用 router.navigate,包括 React 之外。

预加载

策略:intent(悬停/触摸开始)、viewport(交叉观察器)、render(挂载时)。

全局设置:

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

const router = createRouter({
  routeTree,
  defaultPreload: 'intent',
  defaultPreloadDelay: 50, // ms, default is 50
})

或针对单个链接设置:

<Link
  to="/posts/$postId"
  params={{ postId }}
  preload="intent"
  preloadDelay={100}
>
  View Post
</Link>

预加载的数据默认会在 30 秒内保持新鲜(defaultPreloadStaleTime: 30_000)。在此期间不会重新获取数据。如果使用 TanStack Query 等外部缓存,请将 defaultPreloadStaleTime: 0 设为 0,让外部库控制数据新鲜度。

通过路由器实例手动预加载:

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

function Component() {
  const router = useRouter()

  useEffect(() => {
    router.preloadRoute({ to: '/posts/$postId', params: { postId: '1' } })
  }, [router])

  return <div />
}
阻止导航

表单有未保存的更改时,使用 useBlocker 阻止导航:

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('Are you sure you want to leave?')
      return !shouldLeave
    },
  })

  return <form>{/* ... */}</form>
}

使用 withResolver 自定义 UI:

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

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

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

  return (
    <>
      <form>{/* ... */}</form>
      {status === 'blocked' && (
        <div>
          <p>Are you sure you want to leave?</p>
          <button onClick={proceed}>Yes</button>
          <button onClick={reset}>No</button>
        </div>
      )}
    </>
  )
}

单独控制 beforeunload:

useBlocker({
  shouldBlockFn: () => formIsDirty,
  enableBeforeUnload: formIsDirty,
})
使用 linkOptions 复用导航选项

linkOptions 可对导航选项对象进行即时类型检查,因此错误会在定义时而非展开时显现:

import {
  linkOptions,
  Link,
  useNavigate,
  redirect,
} from '@tanstack/react-router'

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

// Use anywhere: Link, navigate, redirect
function Nav() {
  const navigate = useNavigate()

  return (
    <div>
      <Link {...dashboardLinkOptions}>Dashboard</Link>
      <button onClick={() => navigate(dashboardLinkOptions)}>Go</button>
    </div>
  )
}

// Also works in an array for navigation bars
const navOptions = linkOptions([
  { to: '/dashboard', label: 'Summary', activeOptions: { exact: true } },
  { to: '/dashboard/invoices', label: 'Invoices' },
  { to: '/dashboard/users', label: 'Users' },
])

function NavBar() {
  return (
    <nav>
      {navOptions.map((option) => (
        <Link
          {...option}
          key={option.to}
          activeProps={{ className: 'font-bold' }}
        >
          {option.label}
        </Link>
      ))}
    </nav>
  )
}
使用 createLink 创建自定义组件

将任意组件包装为具有 TanStack Router 类型安全导航功能的组件:

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

interface BasicLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {}

const BasicLinkComponent = React.forwardRef<HTMLAnchorElement, BasicLinkProps>(
  (props, ref) => {
    return <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} />
}

使用时仍保留完整的类型安全:

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

在路由器上全局启用:

const router = createRouter({
  routeTree,
  scrollRestoration: true,
})

针对嵌套的可滚动区域:

const router = createRouter({
  routeTree,
  scrollRestoration: true,
  scrollToTopSelectors: ['#main-scrollable-area'],
})

自定义缓存键:

const router = createRouter({
  routeTree,
  scrollRestoration: true,
  getScrollRestorationKey: (location) => location.pathname,
})

针对特定导航阻止滚动重置:

<Link to="/posts" resetScroll={false}>
  Posts
</Link>
使用 MatchRoute 显示待处理 UI
import { Link, MatchRoute } from '@tanstack/react-router'

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

常见错误

严重:将 params 插入 to 字符串
// WRONG — breaks type safety and param encoding
<Link to={`/posts/${postId}`}>Post</Link>

// CORRECT — use the params option
<Link to="/posts/$postId" params={{ postId }}>Post</Link>

动态片段使用 $ 在路由路径中声明。始终通过 params 传递它们。此规则适用于 Link、useNavigate、Navigate 和 router.navigate。

中等:对可点击元素使用 useNavigate
// WRONG — no href, no cmd+click, no preloading, no accessibility
function BadNav() {
  const navigate = useNavigate()
  return <button onClick={() => navigate({ to: '/posts' })}>Posts</button>
}

// CORRECT — real <a> tag with href, accessible, preloadable
function GoodNav() {
  return <Link to="/posts">Posts</Link>
}

仅在以编程方式通过副作用触发导航时(表单提交后、异步操作后等)使用 useNavigate。

高:相对导航未提供 from
// WRONG — without from, ".." resolves from root
<Link to="..">Back</Link>

// CORRECT — provide from for relative resolution
<Link from={Route.fullPath} to="..">Back</Link>

如果不提供 from,只有绝对路径会获得自动补全和类型安全。.. 等相对路径会从根路径而非当前路由解析。

高:将 search 用作对象会丢失已有参数
// WRONG — replaces ALL search params with just { page: 2 }
<Link to="." search={{ page: 2 }}>Page 2</Link>

// CORRECT — preserves existing search params, updates page
<Link to="." search={(prev) => ({ ...prev, page: 2 })}>Page 2</Link>

将 search 作为普通对象传递时,会替换所有搜索参数。请使用函数形式展开之前的参数,并有选择地更新。


交叉引用

  • 另请参阅:router-core/search-params/SKILL.md — Link 的 search 属性会与搜索参数验证交互
  • 另请参阅:router-core/type-safety/SKILL.md — from 收窄可改善 Link 的类型推断

评论

支持 Markdown