讲解

React 本身没有路由,社区的事实标准是 React Router(v7 已融合 Remix 的能力,但核心的声明式路由用法没变)。核心概念和 Vue Router 对仗:路由表(path → 组件)、<Outlet>(子路由渲染出口)、<Link>(声明式导航,代替 a 标签避免整页刷新)。

现代写法用 createBrowserRouter 配路由对象数组:{ path: '/', element: <Layout/>, children: [...] } 嵌套路由让布局组件只写一次,<Outlet> 处渲染匹配的子路由。动态参数 path: 'user/:id',组件里 useParams() 取参;查询字符串用 useSearchParams;编程式导航 useNavigate:navigate('/login')navigate(-1) 后退。404 兜底用 path: '*'

React Router 的数据 API 是它的差异化能力:路由可以配 loader(渲染前加载数据)和 action(处理表单提交),组件里 useLoaderData() 拿数据——数据在导航开始时就开始拉取,而不是组件挂载后才开始(消除「渲染→请求」瀑布)。守卫模式随之改变:登录校验写在 loader 里 redirect('/login'),而不是组件里 useEffect 检查。

懒加载用 React.lazy + Suspense 包路由组件,或者 v7 的 lazy 路由属性。路径相对性有个小坑:嵌套路由里的 <Link to="settings"> 是相对当前路由的,想绝对跳转写 to="/settings"

示例

实现迷你路由匹配 + loader 数据预加载,看「导航即加载」如何避免瀑布(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// 路径匹配(同 Vue 章节的思路,这里支持嵌套树)
function match(routes, path) {
  for (const r of routes) {
    if (r.path === '*') continue; // 通配兜底由路由器单独处理,这里只匹配具体路径
    const keys = [];
    const pattern = r.path
      .split('/')
      .map((s) => {
        if (s.startsWith(':')) {
          keys.push(s.slice(1));
          return '([^/]+)';
        }
        return s;
      })
      .join('/');
    const m = path.match(new RegExp('^' + pattern + '$'));
    if (m) return { route: r, params: Object.fromEntries(keys.map((k, i) => [k, m[i + 1]])) };
  }
  return null;
}

const routes = [
  { path: '/', page: 'Home' },
  { path: '/user/:id', page: 'User' },
  { path: '*', page: 'NotFound' },
];

assert.strictEqual(match(routes, '/user/42').params.id, '42');
assert.strictEqual(match(routes, '/nope'), null); // 无匹配时交给 path: '*' 兜底路由

// loader 模式:匹配时就开始加载数据,渲染时数据已就绪
async function navigate(path, loaders) {
  const started = Date.now();
  const data = await loaders[path](); // 导航即加载
  return { page: path, data, elapsed: Date.now() - started };
}
const wait = (ms, v) => () => new Promise((r) => setTimeout(() => r(v), ms));
const result = await navigate('/user/42', { '/user/42': wait(60, { name: '小明' }) });
assert.strictEqual(result.data.name, '小明');
assert.ok(result.elapsed >= 55);

// 对比瀑布模式:先渲染(30ms)再请求(60ms) = 90ms;loader 模式只有 60ms
const waterfall = 30 + 60;
const loaderTime = 60;
assert.ok(loaderTime < waterfall);

console.log('匹配 /user/42 →', JSON.stringify(match(routes, '/user/42')));
console.log('loader 模式', loaderTime, 'ms vs 瀑布模式', waterfall, 'ms——数据与导航并行');

真实的 React Router 配置:

import { createBrowserRouter, Link, Outlet, useLoaderData, useParams } from 'react-router';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      {
        path: 'user/:id',
        loader: async ({ params }) => {
          const res = await fetch('/api/users/' + params.id); // 导航即加载
          if (!res.ok) throw new Response('Not Found', { status: 404 });
          return res.json();
        },
        element: <User />,
      },
      { path: '*', element: <NotFound /> },
    ],
  },
]);

function Layout() {
  return (
    <>
      <nav>
        <Link to="/">首页</Link>
      </nav>
      <Outlet />
    </>
  );
}

function User() {
  const user = useLoaderData(); // loader 的数据,无瀑布
  const { id } = useParams();
  return <h1>{user.name}(#{id})</h1>;
}

常见坑

  • 用 a 标签站内跳转:整页刷新状态全丢;用 Link 或 useNavigate。
  • 相对路径踩坑:嵌套路由里 to="settings" 是相对路径,确认要相对还是绝对(/ 开头)。
  • 组件里 useEffect 检查登录:跳转闪烁(先渲染再跳走);守卫写 loader 里 redirect。
  • loader 里读 localStorage 做鉴权:SSR 时没有 localStorage,鉴权信息走 cookie/请求。
  • v6 老写法 Routes/Routes 与新数据路由混用:createBrowserRouter 体系才有 loader/action,选一个体系用到底。

小结

createBrowserRouter + 嵌套路由 + Outlet/Link;loader 让数据与导航并行消除瀑布;动态参数 useParams,编程导航 useNavigate。下一章讲全局状态管理的选型。