React Router 实现了“客户端路由”。
在传统的网站中,浏览器从 Web 服务器请求文档,下载并评估 CSS 和 JavaScript 资产,然后渲染从服务器发送的 HTML。当用户点击链接时,它会为新页面重新开始整个过程。
客户端路由允许您的应用程序从链接点击更新 URL,而无需从服务器请求另一个文档。相反,您的应用程序可以立即渲染一些新的 UI 并使用 fetch 发出数据请求以使用新信息更新页面。
这可以实现更快的用户体验,因为浏览器不需要为下一页请求全新的文档或重新评估 CSS 和 JavaScript 资产。它还使用动画等功能实现了更动态的用户体验。
客户端路由通过创建 Router 并使用 Link 和 <Form> 链接/提交页面来实现。
import * as React from "react";
import { createRoot } from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
  Route,
  Link,
} from "react-router-dom";
const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <div>
        <h1>Hello World</h1>
        <Link to="about">About Us</Link>
      </div>
    ),
  },
  {
    path: "about",
    element: <div>About</div>,
  },
]);
createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);
嵌套路由是将 URL 片段与组件层次结构和数据耦合的通用概念。React Router 的嵌套路由灵感来自 2014 年左右的 Ember.js 中的路由系统。Ember 团队意识到,在几乎所有情况下,URL 片段都会决定
React Router 通过 API 接受这种约定,用于创建与 URL 片段和数据耦合的嵌套布局。
// Configure nested routes with JSX
createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="contact" element={<Contact />} />
      <Route
        path="dashboard"
        element={<Dashboard />}
        loader={({ request }) =>
          fetch("/api/dashboard.json", {
            signal: request.signal,
          })
        }
      />
      <Route element={<AuthLayout />}>
        <Route
          path="login"
          element={<Login />}
          loader={redirectIfUser}
        />
        <Route path="logout" action={logoutUser} />
      </Route>
    </Route>
  )
);
// Or use plain objects
createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "contact",
        element: <Contact />,
      },
      {
        path: "dashboard",
        element: <Dashboard />,
        loader: ({ request }) =>
          fetch("/api/dashboard.json", {
            signal: request.signal,
          }),
      },
      {
        element: <AuthLayout />,
        children: [
          {
            path: "login",
            element: <Login />,
            loader: redirectIfUser,
          },
          {
            path: "logout",
            action: logoutUser,
          },
        ],
      },
    ],
  },
]);
这个 可视化 可能会有所帮助。
URL 的片段可以是动态占位符,这些占位符会被解析并提供给各种 API。
<Route path="projects/:projectId/tasks/:taskId" />
使用 : 的两个片段是动态的,并提供给以下 API
// If the current location is /projects/abc/tasks/3
<Route
  // sent to loaders
  loader={({ params }) => {
    params.projectId; // abc
    params.taskId; // 3
  }}
  // and actions
  action={({ params }) => {
    params.projectId; // abc
    params.taskId; // 3
  }}
  element={<Task />}
/>;
function Task() {
  // returned from `useParams`
  const params = useParams();
  params.projectId; // abc
  params.taskId; // 3
}
function Random() {
  const match = useMatch(
    "/projects/:projectId/tasks/:taskId"
  );
  match.params.projectId; // abc
  match.params.taskId; // 3
}
查看
当将 URL 与路由匹配时,React Router 会根据片段数量、静态片段、动态片段、通配符等对路由进行排名,并选择最具体的匹配项。
例如,考虑以下两个路由
<Route path="/teams/:teamId" />
<Route path="/teams/new" />
现在,假设 URL 是 http://example.com/teams/new。
即使这两个路由在技术上都与 URL 匹配(new 可以是 :teamId),您直观地知道我们希望选择第二个路由(/teams/new)。React Router 的匹配算法也知道这一点。
使用排名路由,您无需担心路由顺序。
大多数 Web 应用程序在 UI 的顶部、侧边栏以及通常的多个级别都有持久导航部分。使用 <NavLink> 可以轻松地为活动导航项设置样式,以便用户知道他们在应用程序中的位置 (isActive) 或他们将要前往的位置 (isPending)。
<NavLink
  style={({ isActive, isPending }) => {
    return {
      color: isActive ? "red" : "inherit",
    };
  }}
  className={({ isActive, isPending }) => {
    return isActive ? "active" : isPending ? "pending" : "";
  }}
/>
您还可以 useMatch 用于链接之外的任何其他“活动”指示。
function SomeComp() {
  const match = useMatch("/messages");
  return <li className={Boolean(match) ? "active" : ""} />;
}
查看
与 HTML <a href> 一样,<Link to> 和 <NavLink to> 可以接受相对路径,并具有嵌套路由的增强行为。
假设以下路由配置
<Route path="home" element={<Home />}>
  <Route path="project/:projectId" element={<Project />}>
    <Route path=":taskId" element={<Task />} />
  </Route>
</Route>
考虑 URL https://example.com/home/project/123,它会渲染以下路由组件层次结构
<Home>
  <Project />
</Home>
如果 <Project /> 渲染以下链接,则链接的 href 将按如下方式解析
| 在 <Project>@/home/project/123中 | 解析后的 <a href> | 
|---|---|
| <Link to="abc"> | /home/project/123/abc | 
| <Link to="."> | /home/project/123 | 
| <Link to=".."> | /home | 
| <Link to=".." relative="path"> | /home/project | 
请注意,第一个 .. 会删除 project/:projectId 路由的两个片段。默认情况下,相对链接中的 .. 会遍历路由层次结构,而不是 URL 片段。在下一个示例中添加 relative="path" 允许您遍历路径片段。
相对链接始终相对于它们渲染的路由路径,而不是相对于完整的 URL。这意味着,如果用户使用 <Link to="abc"> 导航到更深层的 <Task />,URL 为 /home/project/123/abc,则 <Project> 中的 href 不会改变(与普通的 <a href> 相反,这是客户端路由器的一个常见问题)。
由于 URL 片段通常映射到应用程序的持久数据,因此 React Router 提供了传统的 data loading hook,以便在导航期间启动 data loading。与嵌套路由结合使用,可以在特定 URL 下为多个布局并行加载所有数据。
<Route
  path="/"
  loader={async ({ request }) => {
    // loaders can be async functions
    const res = await fetch("/api/user.json", {
      signal: request.signal,
    });
    const user = await res.json();
    return user;
  }}
  element={<Root />}
>
  <Route
    path=":teamId"
    // loaders understand Fetch Responses and will automatically
    // unwrap the res.json(), so you can simply return a fetch
    loader={({ params }) => {
      return fetch(`/api/teams/${params.teamId}`);
    }}
    element={<Team />}
  >
    <Route
      path=":gameId"
      loader={({ params }) => {
        // of course you can use any data store
        return fakeSdk.getTeam(params.gameId);
      }}
      element={<Game />}
    />
  </Route>
</Route>
数据通过 useLoaderData 提供给您的组件。
function Root() {
  const user = useLoaderData();
  // data from <Route path="/">
}
function Team() {
  const team = useLoaderData();
  // data from <Route path=":teamId">
}
function Game() {
  const game = useLoaderData();
  // data from <Route path=":gameId">
}
当用户访问或点击链接到 https://example.com/real-salt-lake/45face3 时,所有三个路由加载器都将被调用并并行加载,然后渲染该 URL 的 UI。
在加载或更改数据时,通常需要将用户重定向到不同的路由。
<Route
  path="dashboard"
  loader={async () => {
    const user = await fake.getUser();
    if (!user) {
      // if you know you can't render the route, you can
      // throw a redirect to stop executing code here,
      // sending the user to a new route
      throw redirect("/login");
    }
    // otherwise continue
    const stats = await fake.getDashboardStats();
    return { user, stats };
  }}
/>
<Route
  path="project/new"
  action={async ({ request }) => {
    const data = await request.formData();
    const newProject = await createProject(data);
    // it's common to redirect after actions complete,
    // sending the user to the new record
    return redirect(`/projects/${newProject.id}`);
  }}
/>
查看
当用户在应用程序中导航时,下一页的数据会在页面渲染之前加载。在此期间提供用户反馈非常重要,这样应用程序就不会感觉没有响应。
function Root() {
  const navigation = useNavigation();
  return (
    <div>
      {navigation.state === "loading" && <GlobalSpinner />}
      <FakeSidebar />
      <Outlet />
      <FakeFooter />
    </div>
  );
}
查看
<Suspense> 的骨架 UI您可以 defer 数据,而不是等待下一页的数据,这样 UI 就可以立即翻转到下一个屏幕,并显示占位符 UI,同时数据正在加载。
<Route
  path="issue/:issueId"
  element={<Issue />}
  loader={async ({ params }) => {
    // these are promises, but *not* awaited
    const comments = fake.getIssueComments(params.issueId);
    const history = fake.getIssueHistory(params.issueId);
    // the issue, however, *is* awaited
    const issue = await fake.getIssue(params.issueId);
    // defer enables suspense for the un-awaited promises
    return defer({ issue, comments, history });
  }}
/>;
function Issue() {
  const { issue, history, comments } = useLoaderData();
  return (
    <div>
      <IssueDescription issue={issue} />
      {/* Suspense provides the placeholder fallback */}
      <Suspense fallback={<IssueHistorySkeleton />}>
        {/* Await manages the deferred data (promise) */}
        <Await resolve={history}>
          {/* this calls back when the data is resolved */}
          {(resolvedHistory) => (
            <IssueHistory history={resolvedHistory} />
          )}
        </Await>
      </Suspense>
      <Suspense fallback={<IssueCommentsSkeleton />}>
        <Await resolve={comments}>
          {/* ... or you can use hooks to access the data */}
          <IssueComments />
        </Await>
      </Suspense>
    </div>
  );
}
function IssueComments() {
  const comments = useAsyncValue();
  return <div>{/* ... */}</div>;
}
查看
HTML 表单是导航事件,就像链接一样。React Router 支持使用客户端路由的 HTML 表单工作流程。
当提交表单时,会阻止正常的浏览器导航事件,并创建一个带有包含提交的 FormData 的主体的 Request。此请求将发送到与表单的 <Form action> 匹配的 <Route action>。
表单元素的 name 属性将提交到操作
<Form action="/project/new">
  <label>
    Project title
    <br />
    <input type="text" name="title" />
  </label>
  <label>
    Target Finish Date
    <br />
    <input type="date" name="due" />
  </label>
</Form>
正常的 HTML 文档请求将被阻止,并发送到匹配路由的操作(与 <form action> 匹配的 <Route path>),包括 request.formData。
<Route
  path="project/new"
  action={async ({ request }) => {
    const formData = await request.formData();
    const newProject = await createProject({
      title: formData.get("title"),
      due: formData.get("due"),
    });
    return redirect(`/projects/${newProject.id}`);
  }}
/>
几十年的 Web 惯例表明,当将表单发布到服务器时,数据正在更改,并且会渲染一个新页面。React Router 的基于 HTML 的数据变动 API 遵循了这一惯例。
在调用路由操作后,将再次调用页面上所有数据的加载器,以确保 UI 自动与数据保持最新。无需过期缓存键,无需重新加载上下文提供者。
查看
当将表单提交到路由操作时,您可以访问导航状态以显示繁忙指示器、禁用字段集等。
function NewProjectForm() {
  const navigation = useNavigation();
  const busy = navigation.state === "submitting";
  return (
    <Form action="/project/new">
      <fieldset disabled={busy}>
        <label>
          Project title
          <br />
          <input type="text" name="title" />
        </label>
        <label>
          Target Finish Date
          <br />
          <input type="date" name="due" />
        </label>
      </fieldset>
      <button type="submit" disabled={busy}>
        {busy ? "Creating..." : "Create"}
      </button>
    </Form>
  );
}
查看
了解发送到 action 的 formData 通常足以跳过繁忙指示器,并立即在下一个状态中渲染 UI,即使您的异步工作仍在进行中。这称为“乐观 UI”。
function LikeButton({ tweet }) {
  const fetcher = useFetcher();
  // if there is `formData` then it is posting to the action
  const liked = fetcher.formData
    ? // check the formData to be optimistic
      fetcher.formData.get("liked") === "yes"
    : // if its not posting to the action, use the record's value
      tweet.liked;
  return (
    <fetcher.Form method="post" action="toggle-liked">
      <button
        type="submit"
        name="liked"
        value={liked ? "yes" : "no"}
      />
    </fetcher.Form>
  );
}
(是的,HTML 按钮可以有 name 和 value)。
虽然使用 fetcher 进行乐观 UI 更常见,但您也可以使用普通表单使用 navigation.formData 做到同样的事情。
HTML 表单是变动的模型,但它们有一个主要限制:一次只能有一个,因为表单提交是导航。
大多数 Web 应用程序需要允许同时发生多个变动,例如记录列表,其中每个记录可以独立删除、标记为已完成、喜欢等。
获取器 允许您与路由 操作 和 加载器 交互,而不会在浏览器中引起导航,但仍然可以获得所有传统的好处,例如错误处理、重新验证、中断处理和竞争条件处理。
想象一个任务列表
function Tasks() {
  const tasks = useLoaderData();
  return tasks.map((task) => (
    <div>
      <p>{task.name}</p>
      <ToggleCompleteButton task={task} />
    </div>
  ));
}
每个任务都可以独立于其他任务标记为已完成,并具有自己的挂起状态,而不会使用 获取器 引起导航。
function ToggleCompleteButton({ task }) {
  const fetcher = useFetcher();
  return (
    <fetcher.Form method="post" action="/toggle-complete">
      <fieldset disabled={fetcher.state !== "idle"}>
        <input type="hidden" name="id" value={task.id} />
        <input
          type="hidden"
          name="status"
          value={task.complete ? "incomplete" : "complete"}
        />
        <button type="submit">
          {task.status === "complete"
            ? "Mark Incomplete"
            : "Mark Complete"}
        </button>
      </fieldset>
    </fetcher.Form>
  );
}
查看
React Router 会自动取消陈旧的操作,并且只提交最新数据。
只要有异步 UI,就会有发生竞争条件的风险:当异步操作在较早操作之后开始,但在较早操作之前完成时。结果是用户界面显示了错误的状态。
考虑一个搜索字段,它会在用户键入时更新列表
?q=ry    |---------------|
                         ^ commit wrong state
?q=ryan     |--------|
                     ^ lose correct state
即使 q?=ryan 的查询后来发出,但它先完成。如果处理不当,结果将短暂地显示 ?q=ryan 的正确值,然后翻转到 ?q=ry 的不正确结果。节流和去抖动还不够(您仍然可以中断通过的请求)。您需要取消。
如果您使用 React Router 的数据约定,您将完全自动避免此问题。
?q=ry    |-----------X
                     ^ cancel wrong state when
                       correct state completes earlier
?q=ryan     |--------|
                     ^ commit correct state
React Router 不仅处理此类导航的竞争条件,而且还处理许多其他情况,例如加载自动完成的结果或使用 fetcher(及其自动的并发重新验证)执行多个并发变动。
React Router 会自动处理应用程序的大多数错误。它会捕获在以下情况下抛出的任何错误:
实际上,这几乎是您应用程序中的所有错误,除了在事件处理程序(<button onClick>)或 useEffect 中抛出的错误。React Router 应用程序往往很少使用其中任何一个。
当抛出错误时,React Router 会渲染 errorElement,而不是渲染路由的 element。
<Route
  path="/"
  loader={() => {
    something.that.throws.an.error();
  }}
  // this will not be rendered
  element={<HappyPath />}
  // but this will instead
  errorElement={<ErrorBoundary />}
/>
如果路由没有 errorElement,错误将冒泡到具有 errorElement 的最近的父路由。
<Route
  path="/"
  element={<HappyPath />}
  errorElement={<ErrorBoundary />}
>
  {/* Errors here bubble up to the parent route */}
  <Route path="login" element={<Login />} />
</Route>
查看
React Router 会在导航时模拟浏览器的滚动恢复,等待数据加载后再滚动。这确保滚动位置恢复到正确的位置。
您还可以通过根据除位置之外的其他内容(例如 URL 路径名)进行恢复,以及阻止在某些链接(例如页面中间的选项卡)上发生滚动来自定义行为。
查看
React Router 是基于 Web 标准 API 构建的。 加载器 和 操作 接收标准 Web Fetch API Request 对象,并且也可以返回 Response 对象。取消是使用 中止信号 完成的,搜索参数是使用 URLSearchParams 处理的,数据变动是使用 HTML 表单 处理的。
当您对 React Router 越来越熟悉时,您也会对 Web 平台越来越熟悉。