英文官网地址:
https://reactrouter.com/home

下面示例代码全部基于 v6 最新稳定 API
思维导图:

React Router v6
├─ 入口模式
│  ├─ Data Router  (createBrowserRouter → RouterProvider)  ⭐ 推荐
│  ├─ 组件式       (BrowserRouter → Routes → Route)
│  ├─ Hash         (createHashRouter)
│  └─ Memory       (createMemoryRouter)         ➜ 测试/SSR/RN
├─ 导航
│  ├─ 声明式: Link / NavLink
│  └─ 命令式: useNavigate / redirect (in loader/action)
├─ 参数
│  ├─ 路径参数: useParams
│  └─ 查询参数: useSearchParams
├─ 数据
│  ├─ 预加载: loader + useLoaderData
│  ├─ 写数据: action + useActionData / useSubmit / <Form>
│  └─ 独立请求: useFetcher
└─ 错误
   └─ errorElement + useRouteError

一、入口配置的不同写法说明

1.Data Router

对象配置,官方推荐,支持 loader、action、errorBoundary

// main.jsx
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'

const router = createBrowserRouter([
  { path: '/', element: <Home /> },
  { path: '/about', element: <About /> }
])

/* 
 * createBrowserRouter  创建路由,支持 loader/action/errorElement
 * RouterProvider       把 router 实例注入 React 树,内部用 context 向下传递
 * router               不可变对象,换路由=换树,React 18 可配合 Streaming SSR
 */
ReactDOM.createRoot(document.getElementById('root')).render(
  <RouterProvider router={router} />
)
// 官网路由对象说明:https://reactrouter.com/start/data/route-object
interface RouteObject {
  path?: string;              // 匹配路径,例 "/home", "/users/:id"
  index?: boolean;            // 是否为父路由的默认索引路由(index=true 时 path 被忽略)
  element?: React.ReactNode;  // 要渲染的组件/元素,例 <Home />
  Component?: React.ComponentType; // 同 element,但用类/函数名(官方推荐用 element)
  children?: RouteObject[];   // 子路由,嵌套时用
  loader?: (args: LoaderFunctionArgs) => Promise<Data> | Data; // 进入页面前拉数据
  action?: (args: ActionFunctionArgs) => Promise<Data> | Data | Response | redirect; // 表单提交/写数据
  errorElement?: React.ReactNode; // 当前路由或其 loader/action 抛错时展示的组件
  ErrorBoundary?: React.ComponentType; // 同上,只是用类组件形式
  handle?: any;               // 自定义句柄,可放标题、权限、面包屑等任意元数据
  id?: string;                // 路由唯一 id,一般自动生成,也可手动指定
  // 以下两个高级异步用到再看
  lazy?: () => Promise<{ loader?: Loader; action?: Action; Component?: Component; etc... }>;
  shouldRevalidate?: ShouldRevalidateFunction; // 决定是否重新执行 loader
}

2.组件式

声明式,适合小项目 / 渐进升级

// 组件式:完全用 JSX 写路由,无需 createBrowserRouter
import { BrowserRouter, Routes, Route } from 'react-router-dom'

root.render(
  <BrowserRouter>          {/* 创建 history,提供 context */}
    <Routes>               {/* 匹配唯一 Route,在v5版本中,使用Switch标签 */}
      <Route path="/" element={<Home />} /> {/* 在v5版本中,会使用属性exact来表示完全匹配,避免访问访问 /about 时,Home也会渲染 */}
      <Route path="/about" element={<About />} />
    </Routes>
  </BrowserRouter>
)

特点:

  • 写起来直观,但 不支持 loader/action/errorElement(v6.4+ 新特性只能用 Data Router)。
  • 源码层面 BrowserRouter 内部其实还是 createBrowserRouter 的封装 。

3.Hash Router

文件协议 / 旧服务器无法重定向时用

// Hash 模式
import { createHashRouter, RouterProvider } from 'react-router-dom'
const router = createHashRouter([])   // URL 带 #,刷新 404 问题最小
root.render(<RouterProvider router={router} />)

4.Memory Router

单元测试 / React Native / SSR 无浏览器 history

// 4️⃣ 内存路由
import { createMemoryRouter, RouterProvider } from 'react-router-dom'
const router = createMemoryRouter([], { initialEntries: ['/'] })
/* 测试示例
 * render(<RouterProvider router={router}>)
 * router.navigate('/about')   // 不会改地址栏,纯内存
 */

二、常用Hook/组件介绍

名称 作用 只能在 Data Router 用? 典型用法
useNavigate 编程式跳转 const nav=useNavigate(); nav('/about',{replace:true})
useLocation 拿到当前 location(pathname/search/hash) const {search}=useLocation()
useParams 取动态参数 :id const {id}=useParams()
useSearchParams 读/写 queryString(像 URLSearchParams) const [sp,setSp]=useSearchParams()
useLoaderData 获取当前路由 loader 的返回值 const data=useLoaderData()
useActionData 获取当前路由 action 的返回值(表单 POST 后) const msg=useActionData()
useRouteError 在 errorElement 内读取抛出的错误 const err=useRouteError()
useSubmit 编程式提交表单(走 action) const submit=useSubmit(); submit(data,{method:'post'})
useFetcher 不切换路由、独立调用 action/loader const fetcher=useFetcher(); fetcher.load('/api')
NavLink 带“活跃状态”的 Link <NavLink to="/about" className={({isActive})=>isActive?'active':''}>

useHistoryReact Router v5 的产物;
useNavigateReact Router v6 的替代品,API 更简洁、语义更清晰,同时支持“跳转”和“回退/替换”。

useHistory和useNavigate对比:

能力 useHistory (v5) useNavigate (v6)
前进/普通跳转 history.push('/path') navigate('/path')
替换当前记录 history.replace('/path') navigate('/path', {replace: true})
后退/前进 history.go(-1) / history.back() navigate(-1)(数字即可)
状态携带 history.push('/path', {foo: 1}) navigate('/path', {state: {foo: 1}})
返回值类型 history 对象(含监听、location 等) 只有 navigate 函数,体积更小
阻塞跳转 history.block('确认离开?') 需用 useBlocker Hook(独立 API)

三、Data Router独特功能介绍

1. loader

在进入路由前并行拉数据,返回值通过 useLoaderData 消费。

{
  path: 'dashboard',
  element: <Dashboard />,
  loader: async () => {
    const res = await fetch('/api/stat')
    if (!res.ok) throw new Response('stat error', { status: 500 })
    return res.json()
  }
}
// src/pages/Dashboard.jsx
// ⬅️ 这里直接拿到 loader 的返回值,无需 useEffect 再请求
  const statData = useLoaderData()

2. action

处理 POST/PUT/DELETE/PATCH(表单或 submit),返回值通过 useActionData 消费;支持重定向。

{
  path: 'login',
  element: <LoginPage />,
  action: async ({ request }) => {
    const body = await request.formData()
    const user = await loginAPI(body)
    if (!user) return { error: '账号错误' }   // 返回数据
    throw redirect('/')                       // 跳转
  }
}
// 登录页组件(src/pages/LoginPage.jsx)
import { Form, useActionData, useSubmit } from 'react-router-dom'

export default function LoginPage() {
  // ⬅️ 接收 action 返回值(成功时无值,失败时有 {error})
  const actionData = useActionData()

  /* 两种提交方式任选其一: */
  // 方式 A:声明式 <Form>(推荐,自动序列化)
  return (
    <>
      <h2>登录</h2>
      {actionData?.error && <p style={{ color: 'red' }}>{actionData.error}</p>}
      <Form method="post" action="/login">
        <div>
          <label>
            用户名:
            <input name="username" type="text" required />
          </label>
        </div>
        <div>
          <label>
            密码:
            <input name="password" type="password" required />
          </label>
        </div>
        <button type="submit">登录</button>
      </Form>
    </>
  )

  // 方式 B:编程式 useSubmit(适合自定义校验/额外逻辑)
  // const submit = useSubmit()
  // const handleValidate = (e: FormEvent) => {
  //   e.preventDefault()
  //   const form = e.currentTarget
  //   const formData = new FormData(form)
  //   submit(formData, { method: 'post', action: '/login' })
  // }
  // 然后把 <Form> 换成 <form onSubmit={handleValidate}> 即可
}

3. errorElement

当前路由或它的 loader/action 抛错时渲染,代替整个白屏。

{
  path: 'dashboard',
  element: <Dashboard />,
  loader: dashboardLoader,
  errorElement: <ErrorBoundary />   // 内部用 useRouteError() 读错误
}
// 错误边界组件(src/components/ErrorBoundary.jsx)
import { useRouteError } from 'react-router-dom'

export default function ErrorBoundary() {
  const error = useRouteError() // 读取错误对象(可以是 Error 或 Response)

  // 区分 HTTP 错误 vs JS 运行时错误
  const isResponse = error instanceof Response

  return (
    <div style={{ padding: 24, background: '#fff1f0', border: '1px solid #ffccc7' }}>
      <h2>💥 出错了!</h2>
      {isResponse ? (
        <p>
          网络异常:{error.status} {error.statusText}
        </p>
      ) : (
        <p>运行时错误:{error.message || error}</p>
      )}
      <details style={{ whiteSpace: 'pre-wrap' }}>
        {isResponse
          ? '请检查接口或 loader 逻辑。'
          : error.stack || '无堆栈信息'}
      </details>
      <button onClick={() => window.location.reload()}>刷新重试</button>
    </div>
  )
}

和React原生的异常处理区别

  • React 原生 <ErrorBoundary> 只能捕捉“渲染阶段”和“生命周期”里抛出的错误;对 异步代码(fetch、setTimeout)、事件回调、loader/action 无效。
  • React Router 的 errorElement + useRouteError 专门捕捉 路由层面异步错误(loader/action 抛错、懒加载模块失败),同时也可顺带捕捉该路由组件渲染时的同步错误;但它 不跨路由级别冒泡到全局 React 树
  1. 范围对比
场景 React 原生 ErrorBoundary React Router errorElement
组件渲染 throw ✅ 捕获 ✅ 捕获
生命周期/componentDidCatch ✅ 捕获 ✅ 捕获
事件回调里 throw ❌ 不捕获 ❌ 不捕获(需自行 try/catch)
loader 里 throw ❌ 捕获不到 ✅ 专属捕获
action 里 throw ❌ 捕获不到 ✅ 专属捕获
懒加载代码分割失败 ✅ 捕获 ✅ 捕获
跨整个 React 树(非路由) ✅ 可以放在根 ❌ 只影响当前路由分支
  1. 捕获时机

React 原生

  • 同步渲染阶段 出现错误 → 触发 getDerivedStateFromError → 渲染备用 UI。
  • 异步错误(fetch 失败、setTimeout 回调里 throw)不会触发,需要你在异步代码里手动 setState 触发渲染错误,或自己包 try/catch。

React Router

  • 路由匹配前 运行 loader → 抛错 → 直接跳过原组件,渲染 errorElement
  • 路由匹配后 组件渲染抛错 → 同样落入 errorElement
  • 不care 事件回调里的错误;那种错误仍需要你用原生 ErrorBoundary 或自己处理。
  1. API 差异

React 原生(类组件)

class MyErrorBoundary extends React.Component {
  static getDerivedStateFromError(error) {
    return { hasError: true }          // 更新 state 使下一次渲染备用 UI
  }
  componentDidCatch(error, info) {
    console.error('全局错误:', error, info)
  }
  render() {
    return this.state.hasError
      ? <p>页面崩溃啦</p>
      : this.props.children
  }
}

React Router(函数组件)

function RouterErrorBoundary() {
  const error = useRouteError()        // 读取 loader/action 或渲染错误
  return <div>路由层错误:{error.message||error.status}</div>
}

// 路由表里挂上去
{ path: 'dashboard', element: <Dashboard />, errorElement: <RouterErrorBoundary /> }
  1. 使用场景速记
  • 页面级“兜底”(按钮点击、定时器、非路由相关)→ 用 React 原生 ErrorBoundary 包在根或大块业务组件外。
  • 数据加载/提交失败、路由懒加载失败 → 用 React Router errorElement 挂在对应路由,用户体验最好(URL 保持,只局部出错误卡片)。
  • 两者可以同时存在
    全局包一个 React ErrorBoundary 防“未知渲染崩溃”,每个路由再放一个 errorElement 专门处理“数据/路由”错误,互不干扰。

开发实践

0. 环境准备

# 创建项目(vite 最快)
npm create vite@latest my-router --template react
cd my-router
npm i
# 安装路由
npm i react-router-dom@6

1. 基础:页面跳转

// main.jsx
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'

const router = createBrowserRouter([
  { path: '/', element: <Home /> },
  { path: '/about', element: <About /> }
])

/* 
 * createBrowserRouter  创建路由,支持 loader/action/errorElement
 * RouterProvider       把 router 实例注入 React 树,内部用 context 向下传递
 * router               不可变对象,换路由=换树,React 18 可配合 Streaming SSR
 */
ReactDOM.createRoot(document.getElementById('root')).render(
  <RouterProvider router={router} />
)

// 跳转示例
/* pages/Home.jsx */
import { Link } from 'react-router-dom'
export default function Home() {
  return (
    <>
      <h1>Home</h1>
      <Link to="/about">跳转到 About</Link>
    </>
  )
}
// 跑通后,地址栏手动输入 `/about` 或点击 Link 都能切换,即最简 SPA 完成 。

2. 布局 + 嵌套路由(Layout + Outlet)

实际项目需要“公共头尾 + 中间换内容”。

// main.jsx  只改 router
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,          // 外壳
    children: [                   // 嵌套子路由
      { index: true, element: <Home /> }, // “/” 默认渲染
      { path: 'about', element: <About /> },
      { path: 'products', element: <Products /> }
    ]
  }
])
/* components/Layout.jsx */
import { Outlet, Link } from 'react-router-dom'
export default function Layout() {
  return (
    <>
      <nav>
        <Link to="/">Home</Link> | <Link to="about">About</Link> | <Link to="products">Products</Link>
      </nav>
      <main style={{ padding: 16 }}>
        <Outlet />   {/* 子路由会渲染到这里 */}
      </main>
    </>
  )
}

此时访问 /products 只会局部刷新 <Outlet/> 区域,头部导航不闪屏 。

3. 动态路由 + 参数读取

// router 加一条
{ path: 'products/:id', element: <ProductDetail /> }
/* pages/ProductDetail.jsx */
import { useParams } from 'react-router-dom'
export default function ProductDetail() {
  const { id } = useParams()          // 取 /products/5 中的 5
  return <h2>商品编号:{id}</h2>
}

Link 写法:
<Link to="/products/5">iPhone 17</Link>
编程式跳转:

import { useNavigate } from 'react-router-dom'
const nav = useNavigate()
nav('/products/6')        // 或 nav(-1) 后退

4. 404 与重定向

// 把下面两条放到路由表最后
{ path: 'old-about', element: <Navigate to="/about" replace /> },
{ path: '*', element: <NotFound /> }   // 任何未匹配路径都会落到这里

5. 数据预加载(loader)——进阶第一步

React Router v6 自带“路由级数据预取”,无需 useEffect。

// router
{
  path: 'dashboard',
  element: <Dashboard />,
  loader: async () => {
    const res = await fetch('/api/stat')
    return res.json()
  }
}
/* pages/Dashboard.jsx */
import { useLoaderData } from 'react-router-dom'
export default function Dashboard() {
  const data = useLoaderData()   // 直接拿到 loader 返回值
  return <div>今日 PV{data.pv}</div>
}

好处:数据在渲染前就 Ready,配合 <Suspense> 可一键做骨架屏 。

6. 路由级懒加载(代码分割)

import { lazy, Suspense } from 'react'
const HeavyPage = lazy(() => import('./pages/HeavyPage'))

// 路由配置
{
  path: 'heavy',
  element: (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyPage />
    </Suspense>
  )
}

打包后 heavy.page.js 会被单独拆包,首屏加载体积瞬间减小 。

7. 权限守卫(高阶组件版)

function RequireAuth({ children }) {
  const isLogin = !!localStorage.getItem('token')
  return isLogin ? children : <Navigate to="/login" replace />
}

// 路由里这样用
{
  path: 'admin',
  element: (
    <RequireAuth>
      <Admin />
    </RequireAuth>
  )
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐