mcpField
R
react-router

react-router

github.com/tanstack开发15.3k597 次安装1 周前更新
代码仓库

关于 react-router

面向 React 应用开发者的 TanStack Router 技能,指导使用 `@tanstack/react-router` 构建基于文件的路由,并遵循其类型推断和客户端优先的数据加载模式。

主要功能

  1. 01配置 Vite 文件路由
  2. 02使用类型安全的路由 Hook
  3. 03读取参数、搜索项和加载数据
  4. 04管理导航与路由状态
  5. 05处理认证上下文与延迟数据

适用场景

  1. 01为 React 应用设置文件路由
  2. 02构建类型安全的导航链接
  3. 03在路由中读取并展示加载数据
  4. 04实现认证路由与导航拦截

安装

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

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

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

详细介绍

React Router(@tanstack/react-router)

此技能基于 router-core 构建。请先阅读 @tanstack/router-core#router-core,了解基础概念。

CRITICAL:TanStack Router 的类型完全由推断得出。切勿进行类型断言,也不要为推断出的值添加类型注解。 CRITICAL:TanStack Router 以客户端为先。Loader 默认在客户端运行,而不是服务器上。 CRITICAL:不要将 @tanstack/react-router 与 react-router-dom/react-router 混淆。它们是完全不同的库,API 也各不相同。

完整设置:使用 Vite 进行基于文件的路由

1. 安装依赖项
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/react-router-devtools
2. 配置 Vite 插件
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    // MUST come before react()
    tanstackRouter({
      target: 'react',
      autoCodeSplitting: true,
    }),
    react(),
  ],
})
3. 创建根路由
// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'

export const Route = createRootRoute({
  component: RootLayout,
})

function RootLayout() {
  return (
    <>
      <nav>
        <Link to="/" className="[&.active]:font-bold">
          Home
        </Link>
        <Link to="/about" className="[&.active]:font-bold">
          About
        </Link>
      </nav>
      <hr />
      <Outlet />
      <TanStackRouterDevtools />
    </>
  )
}
4. 创建路由文件
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
  component: HomePage,
})

function HomePage() {
  return <h1>Welcome Home</h1>
}
// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/about')({
  component: AboutPage,
})

function AboutPage() {
  return <h1>About</h1>
}
5. 创建 Router 实例并注册类型
// src/main.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

const router = createRouter({ routeTree })

// REQUIRED — without this, Link/useNavigate/useSearch have no type safety
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement)
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>,
  )
}

Hooks 参考

所有 Hooks 均从 @tanstack/react-router 导入。

useRouter()

直接访问 Router 实例:

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

function InvalidateButton() {
  const router = useRouter()
  return <button onClick={() => router.invalidate()}>Refresh data</button>
}
useRouterState()

订阅 Router 状态变化。它会公开整个状态,因此会带来性能开销。需要访问匹配项或位置时,优先使用 useMatches 和 useLocation。

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

function LoadingIndicator() {
  const isLoading = useRouterState({ select: (s) => s.isLoading })
  return isLoading ? <div>Loading...</div> : null
}
useNavigate()

以编程方式导航(对于用户可点击的元素,优先使用 <Link>):

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

function AfterSubmit() {
  const navigate = useNavigate()

  const handleSubmit = async () => {
    await saveData()
    navigate({ to: '/posts/$postId', params: { postId: '123' } })
  }

  return <button onClick={handleSubmit}>Save</button>
}
useSearch({ from })

读取经过验证的搜索参数:

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

function Pagination() {
  const { page } = useSearch({ from: '/products' })
  return <span>Page {page}</span>
}
useParams({ from })

读取路径参数:

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

function PostHeader() {
  const { postId } = useParams({ from: '/posts/$postId' })
  return <h2>Post {postId}</h2>
}
useLoaderData({ from })

读取路由 Loader 返回的数据:

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

function PostContent() {
  const { post } = useLoaderData({ from: '/posts/$postId' })
  return <article>{post.content}</article>
}
useMatch({ from })

访问完整的路由匹配项(参数、搜索参数、Loader 数据、上下文):

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

function PostDetails() {
  const match = useMatch({ from: '/posts/$postId' })
  return <div>{match.loaderData.post.title}</div>
}
其他 Hooks

均从 @tanstack/react-router 导入:

  • useMatches() — 所有活动路由匹配项的数组(适用于面包屑导航)
  • useRouteContext({ from }) — 读取来自 beforeLoad 或父路由的上下文
  • useBlocker({ shouldBlockFn }) — 有未保存的更改时阻止导航
  • useCanGoBack() — 返回 boolean,用于检查历史记录中是否有可返回的条目
  • useLocation() — 当前解析后的位置(pathname、search、hash)
  • useLinkProps({ to, params?, search? }) — 获取用于自定义链接元素的 <a> 属性
  • useMatchRoute() — 返回一个函数:matchRoute({ to }) => match | false

组件参考

RouterProvider

在 React 树的顶层挂载 Router:

<RouterProvider router={router} />
Link

具有 <a> 语义且类型安全的导航链接:

<Link to="/posts/$postId" params={{ postId: '42' }}>
  View Post
</Link>
Outlet

渲染匹配的子路由组件:

function Layout() {
  return (
    <div>
      <Sidebar />
      <main>
        <Outlet />
      </main>
    </div>
  )
}
Navigate

声明式重定向组件:

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

function OldPage() {
  return <Navigate to="/new-page" />
}
Await

通过 Suspense 渲染尚未等待的 Loader Promise 返回的延迟数据:

import { Await } from '@tanstack/react-router'
import { Suspense } from 'react'

function PostWithComments() {
  const { deferredComments } = Route.useLoaderData()
  return (
    <div>
      <h1>Post</h1>
      <Suspense fallback={<div>Loading comments...</div>}>
        <Await promise={deferredComments}>
          {(comments) => (
            <ul>
              {comments.map((c) => (
                <li key={c.id}>{c.text}</li>
              ))}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  )
}
CatchBoundary

用于组件级错误处理的错误边界(路由级错误使用路由选项 errorComponent):

import { CatchBoundary } from '@tanstack/react-router'
;<CatchBoundary
  getResetKey={() => 'widget'}
  onCatch={(error) => console.error(error)}
  errorComponent={({ error }) => <div>{String(error)}</div>}
>
  <RiskyWidget />
</CatchBoundary>

React 特定模式

使用 createLink 创建自定义链接组件

将 Link 封装在自定义组件中,同时保留类型安全性:

import { createLink } from '@tanstack/react-router'
import { forwardRef, type ComponentPropsWithoutRef } from 'react'

const StyledLinkComponent = forwardRef<
  HTMLAnchorElement,
  ComponentPropsWithoutRef<'a'>
>((props, ref) => (
  <a ref={ref} {...props} className={`styled-link ${props.className ?? ''}`} />
))

const StyledLink = createLink(StyledLinkComponent)

// Usage — same type-safe props as Link
function Nav() {
  return (
    <StyledLink to="/posts/$postId" params={{ postId: '42' }}>
      Post
    </StyledLink>
  )
}
使用 Router Hooks 的可复用组件

要创建一个可在多个路由中使用 Router Hooks 的组件,请将多个路由路径的联合类型作为 from 属性传入:

function PostIdDisplay({ from }: { from: '/posts/$id' | '/drafts/$id' }) {
  const { id } = useParams({ from })
  return <span>ID: {id}</span>
}

// Usage in different route components
<PostIdDisplay from="/posts/$id" />
<PostIdDisplay from="/drafts/$id" />

此模式无需使用 strict: false(该设置会返回不精确的联合类型),同时也能让组件在指定的已知路由间复用。

Auth Provider 必须包裹 RouterProvider

如果路由使用了 Auth 上下文(通过 createRootRouteWithContext),则 Auth Provider 必须是 RouterProvider 的祖先:

// CORRECT — AuthProvider wraps RouterProvider
function App() {
  return (
    <AuthProvider>
      <RouterProvider router={router} />
    </AuthProvider>
  )
}

// WRONG — RouterProvider outside auth provider
function App() {
  return (
    <RouterProvider router={router}>
      <AuthProvider>{/* ... */}</AuthProvider>
    </RouterProvider>
  )
}

或者使用 Router 选项 Wrap 提供上下文,无需在外部包裹:

const router = createRouter({
  routeTree,
  Wrap: ({ children }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  ),
})

常见错误

1. 高优先级:在 beforeLoad 或 loader 中使用 React Hooks

beforeLoad 和 loader 不是 React 组件,而是普通的异步函数。不能在其中调用 React Hooks。请改为通过 Router 上下文传递 Auth 状态。

// WRONG — useAuth is a React hook, cannot be called here
beforeLoad: () => {
  const auth = useAuth()
  if (!auth.user) throw redirect({ to: '/login' })
}

// CORRECT — read auth from router context
beforeLoad: ({ context }) => {
  if (!context.auth.isAuthenticated) {
    throw redirect({ to: '/login' })
  }
}
2. HIGH:错误地将 RouterProvider 包裹在 auth provider 内

使用 undefined! 占位符创建一次路由器,然后通过 RouterProvider 的 context 属性注入实时 auth。不要在 auth 发生变化时重新创建路由器——这会重置缓存并重建树。

// CORRECT — create router once, inject live auth via context prop
const router = createRouter({
  routeTree,
  context: { auth: undefined! }, // placeholder, filled by RouterProvider
})

function InnerApp() {
  const auth = useAuth()
  return <RouterProvider router={router} context={{ auth }} />
}

function App() {
  return (
    <AuthProvider>
      <InnerApp />
    </AuthProvider>
  )
}
3. MEDIUM:Await/延迟数据缺少 Suspense 边界

Await 需要一个 <Suspense> 祖先。没有它,延迟 Promise 就没有回退 UI,并会抛出错误。

// WRONG — no Suspense boundary
<Await promise={deferredData}>{(data) => <div>{data}</div>}</Await>

// CORRECT — wrap in Suspense
<Suspense fallback={<div>Loading...</div>}>
  <Await promise={deferredData}>{(data) => <div>{data}</div>}</Await>
</Suspense>

交叉引用

  • @tanstack/router-core#router-core — 所有针对特定领域模式的子技能(搜索参数、数据加载、导航、auth、SSR 等)

评论

支持 Markdown