主分支
分支
主分支 (6.23.1)开发分支
版本
6.23.1v4/5.xv3.x
功能概述
本页内容

功能概述

客户端路由

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}`);
  }}
/>

查看

挂起导航 UI

当用户在应用程序中导航时,下一页的数据会在页面渲染之前加载。在此期间提供用户反馈非常重要,这样应用程序就不会感觉没有响应。

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>
  );
}

查看

乐观 UI

了解发送到 actionformData 通常足以跳过繁忙指示器,并立即在下一个状态中渲染 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 按钮可以有 namevalue)。

虽然使用 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 路径名)进行恢复,以及阻止在某些链接(例如页面中间的选项卡)上发生滚动来自定义行为。

查看

Web 标准 API

React Router 是基于 Web 标准 API 构建的。 加载器操作 接收标准 Web Fetch API Request 对象,并且也可以返回 Response 对象。取消是使用 中止信号 完成的,搜索参数是使用 URLSearchParams 处理的,数据变动是使用 HTML 表单 处理的。

当您对 React Router 越来越熟悉时,您也会对 Web 平台越来越熟悉。

搜索参数

待办事项

位置状态

待办事项