主分支
分支
main (6.23.1)dev
版本
6.23.1v4/5.xv3.x
常见问题解答
本页内容

常见问题解答

以下是一些人们关于 React Router v6 的常见问题。您可能还会在 示例 中找到您要查找的内容。

withRouter 发生了什么?我需要它!

这个问题通常源于您使用 React 类组件,这些组件不支持钩子。在 React Router v6 中,我们完全拥抱了钩子,并使用它们来共享路由器的所有内部状态。但这并不意味着您无法使用路由器。假设您实际上可以使用钩子(您使用的是 React 16.8+),您只需要一个包装器。

import {
  useLocation,
  useNavigate,
  useParams,
} from "react-router-dom";

function withRouter(Component) {
  function ComponentWithRouterProp(props) {
    let location = useLocation();
    let navigate = useNavigate();
    let params = useParams();
    return (
      <Component
        {...props}
        router={{ location, navigate, params }}
      />
    );
  }

  return ComponentWithRouterProp;
}

为什么 <Route> 有一个 element 属性而不是 rendercomponent

在 React Router v6 中,我们从使用 v5 的 <Route component><Route render> API 切换到 <Route element>。为什么呢?

首先,我们看到 React 本身在 <Suspense fallback={<Spinner />}> API 中领先。fallback 属性接受一个 React 元素,而不是一个 组件。这使您可以轻松地将您想要的任何属性从渲染它的组件传递到您的 <Spinner>

使用元素而不是组件意味着我们不必提供 passProps 样式的 API,因此您可以将所需的属性传递到您的元素。例如,在基于组件的 API 中,没有好的方法可以将属性传递到 <Route path=":userId" component={Profile} /> 匹配时渲染的 <Profile> 元素。大多数采用这种方法的 React 库最终会使用类似 <Route component={Profile} passProps={{ animate: true }} /> 的 API,或者使用渲染道具或高阶组件。

此外,v5 中 Route 的渲染 API 相当大。当我们开发 v4/5 时,对话是这样的

// Ah, this is nice and simple!
<Route path=":userId" component={Profile} />

// But wait, how do I pass custom props to the <Profile> element??
// Hmm, maybe we can use a render prop in those situations?
<Route
  path=":userId"
  render={routeProps => (
    <Profile routeProps={routeProps} animate={true} />
  )}
/>

// Ok, now we have two ways to render something with a route. :/

// But wait, what if we want to render something when a route
// *doesn't* match the URL, like a Not Found page? Maybe we
// can use another render prop with slightly different semantics?
<Route
  path=":userId"
  children={({ match }) => (
    match ? (
      <Profile match={match} animate={true} />
    ) : (
      <NotFound />
    )
  )}
/>

// What if I want to get access to the route match, or I need
// to redirect deeper in the tree?
function DeepComponent(routeStuff) {
  // got routeStuff, phew!
}
export default withRouter(DeepComponent);

// Well hey, now at least we've covered all our use cases!
// ... *facepalm*

API 扩展的部分原因是 React 没有为我们提供任何方法来从 <Route> 获取信息到您的路由元素,因此我们不得不发明巧妙的方法来将路由数据您自己的自定义属性传递到您的元素:component、渲染道具、passProps 高阶组件……直到钩子出现!

现在,上面的对话是这样的

// Ah, nice and simple API. And it's just like the <Suspense> API!
// Nothing more to learn here.
<Route path=":userId" element={<Profile />} />

// But wait, how do I pass custom props to the <Profile>
// element? Oh ya, it's just an element. Easy.
<Route path=":userId" element={<Profile animate={true} />} />

// Ok, but how do I access the router's data, like the URL params
// or the current location?
function Profile({ animate }) {
  let params = useParams();
  let location = useLocation();
}

// But what about components deep in the tree?
function DeepComponent() {
  // oh right, same as anywhere else
  let navigate = useNavigate();
}

// Aaaaaaaaand we're done here.

在 v6 中使用 element 属性的另一个重要原因是 <Route children> 用于嵌套路由。您可以在 关于 v6 入门的指南 中了解更多信息。

如何在 react-router v6 中添加 No Match (404) 路由?

在 v4 中,我们只需将路径属性从路由中删除。在 v5 中,我们将 404 元素包装在 Route 中并使用 path="*"。在 v6 中,使用 path="*" 并将 404 元素传递到新的 element 属性中,而不是将其包装起来

<Route path="*" element={<NoMatch />} />

<Route> 无法渲染?如何组合?

在 v5 中,<Route> 组件只是一个普通的组件,就像一个 if 语句,当 URL 与其路径匹配时就会渲染。在 v6 中,<Route> 元素实际上永远不会渲染,它只是用于配置。

在 v5 中,由于路由只是组件,因此当路径为 "/my-route" 时,MyRoute 将被渲染。

let App = () => (
  <div>
    <MyRoute />
  </div>
);

let MyRoute = ({ element, ...rest }) => {
  return (
    <Route path="/my-route" children={<p>Hello!</p>} />
  );
};

然而,在 v6 中,<Route> 仅用于其属性,因此以下代码永远不会渲染 <p>Hello!</p>,因为 <MyRoute> 没有 <Routes> 可以看到的路径

let App = () => (
  <Routes>
    <MyRoute />
  </Routes>
);

let MyRoute = () => {
  // won't ever render because the path is down here
  return (
    <Route path="/my-route" children={<p>Hello!</p>} />
  );
};

您可以通过以下方式获得相同的效果

  • 仅在 <Routes> 内渲染 <Route> 元素
  • 将组合移动到 element 属性中
let App = () => (
  <div>
    <Routes>
      <Route path="/my-route" element={<MyRoute />} />
    </Routes>
  </div>
);

let MyRoute = () => {
  return <p>Hello!</p>;
};

<Routes> 中静态地提供完整的嵌套路由配置将使 v6.x 中的许多功能成为可能,因此我们鼓励您将路由放在一个顶级配置中。如果您真的喜欢与任何其他组件无关的匹配 URL 的组件的想法,您可以创建一个类似于 v5 Route 的组件,方法如下

function MatchPath({ path, Comp }) {
  let match = useMatch(path);
  return match ? <Comp {...match} /> : null;
}

// Will match anywhere w/o needing to be in a `<Routes>`
<MatchPath path="/accounts/:id" Comp={Account} />;

如何在树中深度嵌套路由?

在 v5 中,您可以在任何地方渲染 <Route><Switch>。您可以继续执行完全相同的事情,但您需要使用 <Routes>(没有 's' 的 <Route> 将不起作用)。我们称这些为“后代 <Routes>”。

在 v5 中可能看起来像这样

// somewhere up the tree
<Switch>
  <Route path="/users" component={Users} />
</Switch>;

// and now deeper in the tree
function Users() {
  return (
    <div>
      <h1>Users</h1>
      <Switch>
        <Route path="/users/account" component={Account} />
      </Switch>
    </div>
  );
}

在 v6 中几乎相同

  • 请注意祖先路由中的 *,以便即使它没有直接子级,它也能匹配更深的 URL
  • 您不再需要知道整个子路由路径,现在可以使用相对路由
// somewhere up the tree
<Routes>
  <Route path="/users/*" element={<Users />} />
</Routes>;

// and now deeper in the tree
function Users() {
  return (
    <div>
      <h1>Users</h1>
      <Routes>
        <Route path="account" element={<Account />} />
      </Routes>
    </div>
  );
}

如果您在 v5 中有一个“浮动路由”(没有包装在 <Switch> 中),只需将其包装在 <Routes> 中即可。

// v5
<Route path="/contact" component={Contact} />

// v6
<Routes>
  <Route path="contact" element={<Contact />} />
</Routes>

正则表达式路由路径发生了什么?

正则表达式路由路径被删除的原因有两个

  1. 路由中的正则表达式路径为 v6 的排名路由匹配提出了很多问题。如何对正则表达式进行排名?

  2. 我们能够摆脱整个依赖项(path-to-regexp)并显着减少发送到用户浏览器中的包大小。如果将其添加回来,它将占 React Router 页面大小的 1/3!

在查看了许多用例后,我们发现我们仍然可以在没有直接正则表达式路径支持的情况下满足它们,因此我们做出了权衡,以显着减小捆绑包大小并避免围绕排名正则表达式路由的开放问题。

大多数正则表达式路由只关心一次一个 URL 段,并执行以下两件事之一

  1. 匹配多个静态值
  2. 以某种方式验证参数(是数字、不是数字等)

匹配通常的静态值

我们看到的一个非常常见的路由是匹配多个语言代码的正则表达式

function App() {
  return (
    <Switch>
      <Route path={/(en|es|fr)/} component={Lang} />
    </Switch>
  );
}

function Lang({ params }) {
  let lang = params[0];
  let translations = I81n[lang];
  // ...
}

这些实际上都是静态路径,因此在 v6 中,您可以创建三个路由并将代码直接传递给组件。如果您有很多,请创建一个数组并将其映射到路由中,以避免重复。

function App() {
  return (
    <Routes>
      <Route path="en" element={<Lang lang="en" />} />
      <Route path="es" element={<Lang lang="es" />} />
      <Route path="fr" element={<Lang lang="fr" />} />
    </Routes>
  );
}

function Lang({ lang }) {
  let translations = I81n[lang];
  // ...
}

执行某种参数验证

另一个常见情况是确保参数是整数。

function App() {
  return (
    <Switch>
      <Route path={/users\/(\d+)/} component={User} />
    </Switch>
  );
}

function User({ params }) {
  let id = params[0];
  // ...
}

在这种情况下,您必须在匹配组件内部使用正则表达式自己做一些工作

function App() {
  return (
    <Routes>
      <Route path="/users/:id" element={<ValidateUser />} />
      <Route path="/users/*" element={<NotFound />} />
    </Routes>
  );
}

function ValidateUser() {
  let params = useParams();
  let userId = params.id.match(/\d+/);
  if (!userId) {
    return <NotFound />;
  }
  return <User id={params.userId} />;
}

function User(props) {
  let id = props.id;
  // ...
}

在 v5 中,如果正则表达式不匹配,则 <Switch> 将继续尝试匹配下一个路由

function App() {
  return (
    <Switch>
      <Route path={/users\/(\d+)/} component={User} />
      <Route path="/users/new" exact component={NewUser} />
      <Route
        path="/users/inactive"
        exact
        component={InactiveUsers}
      />
      <Route path="/users/*" component={NotFound} />
    </Switch>
  );
}

查看此示例,您可能会担心在 v6 版本中,您的其他路由将不会在其 URL 上渲染,因为 :userId 路由可能会首先匹配。但是,由于路由排名,情况并非如此。“new”和“inactive”路由将排名更高,因此将在其各自的 URL 上渲染

function App() {
  return (
    <Routes>
      <Route path="/users/:id" element={<ValidateUser />} />
      <Route path="/users/new" element={<NewUser />} />
      <Route
        path="/users/inactive"
        element={<InactiveUsers />}
      />
    </Routes>
  );
}

事实上,如果您的路由没有按正确顺序排列,v5 版本会有各种问题。V6 完全消除了这个问题。

Remix 用户

如果您使用的是 Remix,您可以通过将此工作移到您的加载器中,向浏览器发送正确的 40x 响应。这还会减少发送到用户的浏览器捆绑包的大小,因为加载器只在服务器上运行。

import { useLoaderData } from "remix";

export async function loader({ params }) {
  if (!params.id.match(/\d+/)) {
    throw new Response("", { status: 400 });
  }

  let user = await fakeDb.user.find({
    where: { id: params.id },
  });
  if (!user) {
    throw new Response("", { status: 404 });
  }

  return user;
}

function User() {
  let user = useLoaderData();
  // ...
}

Remix 将不会渲染您的组件,而是渲染最近的 catch boundary