主分支
分支
主分支 (6.23.1)开发分支
版本
6.23.1v4/5.xv3.x
从 @reach/router 迁移
本页内容

从 Reach Router 迁移到 React Router v6

此页面正在建设中。请告诉我们哪里不足,以便我们尽可能顺利地完成迁移!

介绍

当我们着手构建 React Router v6 时,从 @reach/router 用户的角度来看,我们有以下目标

  • 保持捆绑包大小较小(事实证明,我们比 @reach/router 更小)
  • 保留 @reach/router 的最佳部分(嵌套路由,以及通过排名路径匹配和 navigate 简化的 API)
  • 更新 API 以符合现代 React(又名钩子)的习惯用法。
  • 为并发模式和 Suspense 提供更好的支持。
  • 默认情况下停止执行不够好的焦点管理。

如果我们要制作一个 @reach/router v2,它看起来几乎与 React Router v6 完全一样。因此,@reach/router 的下一个版本就是 React Router v6。换句话说,不会有 @reach/router v2,因为它将与 React Router v6 相同。

实际上,@reach/router 1.3 和 React Router v6 之间的许多 API 都是相同的。

  • 路由按等级排列并匹配
  • 嵌套路由配置存在
  • navigate 具有相同的签名
  • Link 具有相同的签名
  • 1.3 中的所有钩子都是相同的(或几乎相同)

大多数更改只是重命名。如果您碰巧编写了代码修改器,请与我们分享,我们会将其添加到本指南中!

升级概述

在本指南中,我们将向您展示如何升级路由代码的每个部分。我们将逐步进行,以便您可以进行一些更改,发布,然后在方便时返回进行迁移。我们还将讨论一些关于“为什么”进行更改的内容,以及看似简单的重命名实际上背后有更大的原因。

首先:非破坏性更新

我们强烈建议您在迁移到 React Router v6 之前对代码进行以下更新。这些更改不必在整个应用程序中一次性完成,您只需更新一行代码,提交并发布即可。这样做将大大减少您在进行 React Router v6 中的重大更改时所付出的努力。

  1. 升级到 React v16.8 或更高版本
  2. 升级到 @reach/router v1.3
  3. 更新路由组件以从钩子访问数据
  4. 在应用程序的顶部添加一个 <LocationProvider/>

第二:破坏性更新

以下更改需要在整个应用程序中一次性完成。

  1. 升级到 React Router v6
  2. 将所有 <Router> 元素更新为 <Routes>
  3. <RouteElement default/> 更改为 <RouteElement path="*" />
  4. 修复 <Redirect />
  5. 使用钩子实现 <Link getProps />
  6. 更新 useMatch,参数在 match.params
  7. ServerLocation 更改为 StaticRouter

非破坏性更新

升级到 React v16.8

React Router v6 大量使用 React 钩子,因此您需要在尝试升级到 React Router v6 之前使用 React 16.8 或更高版本。

升级到 React 16.8 后,您应该部署应用程序。然后,您可以稍后回来继续您之前的工作。

升级到 @reach/router v1.3.3

您应该能够简单地安装 v1.3.3,然后部署您的应用程序。

npm install @reach/router@latest

更新路由组件以使用钩子

您可以一次更新一个路由组件,提交并部署。您不需要一次更新整个应用程序。

@reach/router v1.3 中,我们添加了钩子来访问路由数据,为 React Router v6 做准备。如果您先这样做,那么在升级到 React Router v6 时,您将需要做的工作更少。

// @reach/router v1.2
<Router>
  <User path="users/:userId/grades/:assignmentId" />
</Router>;

function User(props) {
  let {
    // route params were accessed from props
    userId,
    assignmentId,

    // as well as location and navigate
    location,
    navigate,
  } = props;

  // ...
}

// @reach/router v1.3 and React Router v6
import {
  useParams,
  useLocation,
  useNavigate,
} from "@reach/router";

function User() {
  // everything comes from a specific hook now
  let { userId, assignmentId } = useParams();
  let location = useLocation();
  let navigate = useNavigate();
  // ...
}

理由

所有这些数据都已存在于上下文中,但从那里访问它们对于应用程序代码来说很尴尬,因此我们将它们转储到您的道具中。钩子使从上下文中访问数据变得简单,因此我们不再需要用路由信息污染您的道具。

不污染道具也有助于 TypeScript,并且还可以防止您在查看组件时想知道道具来自哪里。如果您使用的是来自路由器的数据,现在它已经完全清楚了。

此外,随着页面的增长,您自然会将其分解为多个组件,并最终将数据“道具钻取”到树的底部。现在,您可以在树中的任何位置访问路由数据。这不仅更方便,而且使创建以路由器为中心的可组合抽象成为可能。如果自定义钩子需要位置,它现在可以简单地使用 useLocation() 等来请求它。

添加 LocationProvider

虽然 @reach/router 不需要在应用程序树的顶部使用位置提供程序,但 React Router v6 需要,因此现在就准备好它吧。

// before
ReactDOM.render(<App />, el);

// after
import { LocationProvider } from "@reach/router";

ReactDOM.render(
  <LocationProvider>
    <App />
  </LocationProvider>,
  el
);

理由

@reach/router 使用全局默认历史记录实例,该实例在模块中具有副作用,这会阻止模块的树状摇动,无论您是否使用全局实例。此外,React Router 提供了其他历史记录类型(如哈希历史记录),而 @reach/router 没有,因此它始终需要一个顶级位置提供程序(在 React Router 中,这些是 <BrowserRouter/> 及其朋友)。

此外,各种模块(如 RouterLinkuseLocation)在 <LocationProvider/> 之外渲染,它们会设置自己的 URL 监听器。这通常不是问题,但每一小部分都很重要。在顶部放置一个 <LocationProvider /> 允许应用程序拥有一个 URL 监听器。

破坏性更新

下一组更新需要一次性完成。幸运的是,大多数只是简单的重命名。

您可以使用一个技巧,在迁移时同时使用两个路由器,但您绝对不应该以这种状态发布您的应用程序,因为它们是不可互操作的。您从一个路由器中的链接将无法在另一个路由器中使用。但是,能够进行更改并刷新页面以查看您是否正确执行了这一步,这很好。

安装 React Router v6

npm install react-router@6 react-router-dom@6

LocationProvider 更新为 BrowserRouter

// @reach/router
import { LocationProvider } from "@reach/router";

ReactDOM.render(
  <LocationProvider>
    <App />
  </LocationProvider>,
  el
);

// React Router v6
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  el
);

Router 更新为 Routes

您可能有多个,但通常只有一个在应用程序的顶部附近。如果您有多个,请对每个都执行此操作。

// @reach/router
import { Router } from "@reach/router";

<Router>
  <Home path="/" />
  {/* ... */}
</Router>;

// React Router v6
import { Routes, Route } from "react-router-dom";

<Routes>
  <Route path="/" element={<Home />} />
  {/* ... */}
</Routes>;

更新 default 路由属性

default 属性告诉 @reach/router 如果没有其他路由匹配,则使用该路由。在 React Router v6 中,您可以使用通配符路径来解释此行为。

// @reach/router
<Router>
  <Home path="/" />
  <NotFound default />
</Router>

// React Router v6
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="*" element={<NotFound />} />
</Routes>

<Redirect/>redirectToisRedirect

哇...系好安全带,准备好了。请将您的西红柿留着做自制的马格丽特披萨,而不是扔给我们。

我们已经删除了从 React Router 中重定向的功能。这意味着没有 <Redirect/>redirectToisRedirect,也没有替代 API。请继续阅读 😅

不要将重定向与用户与您的应用程序交互时的导航混淆。响应用户交互进行导航仍然受支持。当我们谈论重定向时,我们指的是在匹配时进行重定向

<Router>
  <Home path="/" />
  <Users path="/events" />
  <Redirect from="/dashboard" to="/events" />
</Router>

@reach/router 中的重定向工作方式有点像实验。它“抛出”重定向,并使用 componentDidCatch 捕获它。这很酷,因为它会导致整个渲染树停止,然后使用新位置重新开始。几年前我们首次发布此项目时,与 React 团队的讨论导致我们尝试了这种方法。

在遇到问题(例如,应用程序级别的 componentDidCatch 需要重新抛出重定向)后,我们决定在 React Router v6 中不再这样做。

但我们更进一步,认为重定向甚至不是 React Router 的工作。您的动态 Web 服务器或静态文件服务器应该处理此问题,并发送适当的响应状态代码,例如 301 或 302。

在 React Router 中匹配时具有重定向的能力,充其量需要您在两个地方配置重定向(您的服务器和您的路由),最糟糕的是鼓励人们只在 React Router 中进行重定向——这根本不会发送状态代码。

我们经常使用 Firebase 托管,因此以下是如何更新我们其中一个应用程序的示例

// @reach/router
<Router>
  <Home path="/" />
  <Users path="/events" />
  <Redirect from="/dashboard" to="/events" />
</Router>
// React Router v6
// firebase.json config file
{
  // ...
  "hosting": {
    "redirects": [
      {
        "source": "/dashboard",
        "destination": "/events",
        "type": 301
      }
    ]
  }
}

无论我们是在使用无服务器函数进行服务器渲染,还是仅将其用作静态文件服务器,这都适用。所有 Web 托管服务都提供配置此功能的方法。

如果您的应用程序仍然存在 <Link to="/events" />,并且用户点击它,由于您使用的是客户端路由器,因此服务器不会参与。您需要更加勤勉地更新您的链接 😬。

或者,如果您想允许过时的链接,并且您意识到您需要在客户端和服务器上配置您的重定向,请继续复制粘贴我们即将发布但随后删除的 Redirect 组件。

import { useEffect } from "react";
import { useNavigate } from "react-router-dom";

function Redirect({ to }) {
  let navigate = useNavigate();
  useEffect(() => {
    navigate(to);
  });
  return null;
}

// usage
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/events" element={<Users />} />
  <Route
    path="/dashboard"
    element={<Redirect to="/events" />}
  />
</Routes>;

理由

我们认为,通过根本不提供任何重定向 API,人们更有可能正确地配置它们。多年来,我们一直在无意中鼓励不良做法,现在我们想停止 🙈。

此道具获取器对于将链接样式化为“活动”很有用。决定链接是否处于活动状态有点主观。有时您希望它在 URL 完全匹配时处于活动状态,有时您希望它在部分匹配时处于活动状态,甚至还有更多涉及搜索参数和位置状态的边缘情况。

// @reach/router
function SomeCustomLink() {
  return (
    <Link
      to="/some/where/cool"
      getProps={(obj) => {
        let {
          isCurrent,
          isPartiallyCurrent,
          href,
          location,
        } = obj;
        // do what you will
      }}
    />
  );
}

// React Router
import { useLocation, useMatch } from "react-router-dom";

function SomeCustomLink() {
  let to = "/some/where/cool";
  let match = useMatch(to);
  let { isExact } = useMatch(to);
  let location = useLocation();
  return <Link to={to} />;
}

让我们看一些不太通用的例子。

// A custom nav link that is active when the URL matches the link's href exactly

// @reach/router
function ExactNavLink(props) {
  const isActive = ({ isCurrent }) => {
    return isCurrent ? { className: "active" } : {};
  };
  return <Link getProps={isActive} {...props} />;
}

// React Router v6
function ExactNavLink(props) {
  return (
    <Link
      // If you only need the active state for styling without
      // overriding the default isActive state, we provide it as
      // a named argument in a function that can be passed to
      // either `className` or `style` props
      className={({ isActive }) =>
        isActive ? "active" : ""
      }
      {...props}
    />
  );
}

// A link that is active when itself or deeper routes are current

// @reach/router
function PartialNavLink(props) {
  const isPartiallyActive = ({ isPartiallyCurrent }) => {
    return isPartiallyCurrent
      ? { className: "active" }
      : {};
  };
  return <Link getProps={isPartiallyActive} {...props} />;
}

// React Router v6
function PartialNavLink(props) {
  // add the wild card to match deeper URLs
  let match = useMatch(props.to + "/*");
  return (
    <Link className={match ? "active" : ""} {...props} />
  );
}

理由

“道具获取器”很笨拙,几乎总是可以用钩子替换。这也允许您使用其他钩子,例如 useLocation,并执行更多自定义操作,例如使用搜索字符串使链接处于活动状态

function RecentPostsLink(props) {
  let match = useMatch("/posts");
  let location = useLocation();
  let isActive =
    match && location.search === "?view=recent";
  return (
    <Link className={isActive ? "active" : ""}>Recent</Link>
  );
}

useMatch

useMatch 的签名在 React Router v6 中略有不同。

// @reach/router
let {
  uri,
  path,

  // params are merged into the object with uri and path
  eventId,
} = useMatch("/events/:eventId");

// React Router v6
let {
  url,
  path,

  // params get their own key on the match
  params: { eventId },
} = useMatch("/events/:eventId");

还要注意从 uri -> url 的更改。

理由

感觉将参数与 URL 和路径分开更干净。

此外,没有人知道 URL 和 URI 之间的区别,因此我们不想开始一堆关于它的迂腐争论。React Router 一直称其为 URL,并且它拥有更多生产应用程序,因此我们使用 URL 而不是 URI。

<Match />

React Router v6 中没有 <Match/> 组件。它使用渲染道具来组合行为,但我们现在有了钩子。

如果您喜欢它,或者只是不想更新您的代码,很容易回溯

function Match({ path, children }) {
  let match = useMatch(path);
  let location = useLocation();
  let navigate = useNavigate();
  return children({ match, location, navigate });
}

理由

现在我们有了钩子,渲染道具有点恶心(ew!)

<ServerLocation />

这里只是简单的重命名

// @reach/router
import { ServerLocation } from "@reach/router";

createServer((req, res) => {
  let markup = ReactDOMServer.renderToString(
    <ServerLocation url={req.url}>
      <App />
    </ServerLocation>
  );
  req.send(markup);
});

// React Router v6
// note the import path from react-router-dom/server!
import { StaticRouter } from "react-router-dom/server";

createServer((req, res) => {
  let markup = ReactDOMServer.renderToString(
    <StaticRouter location={req.url}>
      <App />
    </StaticRouter>
  );
  req.send(markup);
});

反馈!

如果您觉得本指南有所帮助,请告诉我们

打开拉取请求:请添加您需要的任何我们遗漏的迁移。

一般反馈:Twitter 上的 @remix_run,或发送电子邮件至 [email protected]

谢谢!