关于 react-router
面向 React 应用开发者的 TanStack Router 技能,指导使用 `@tanstack/react-router` 构建基于文件的路由,并遵循其类型推断和客户端优先的数据加载模式。
主要功能
- 01配置 Vite 文件路由
- 02使用类型安全的路由 Hook
- 03读取参数、搜索项和加载数据
- 04管理导航与路由状态
- 05处理认证上下文与延迟数据
适用场景
- 01为 React 应用设置文件路由
- 02构建类型安全的导航链接
- 03在路由中读取并展示加载数据
- 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 等)