Skip to content

从v5升级

向后兼容软件包

与一次性升级和更新所有代码(这非常困难,而且容易出错)不同,向后兼容性软件包让您可以通过并行运行 v5 和 v6,一次升级一个组件、一个钩子和一个路由。您未触及的任何代码仍在运行与之前相同的代码。一旦所有组件都完全使用 v6 API,您的应用程序就不再需要兼容性软件包,可以在 v6 上运行了。点击此处查看官方指南。

我们建议使用向后兼容性软件包来升级有多个路径的应用程序。否则,我们希望本指南能帮助您一次性完成升级!

导言

React Router 第 6 版引入了多个强大的新功能,并改进了与最新版本 React 的兼容性。此外,它还引入了一些与第 5 版不同的改动。本文档全面介绍了如何将 v4/5 应用程序升级到 v6,同时希望能在升级过程中尽可能频繁地发布应用程序。

本指南中的示例将展示您如何在 v5 应用程序中构建某些内容的代码示例,以及您如何在 v6 中完成相同内容的代码示例。本指南还将解释我们做出这一改变的原因,以及这一改变将如何改善您的代码和使用您应用程序的用户的整体体验。

一般来说,流程是这样的:

  1. 升级到 React v16.8 或更高版本
  2. 升级到 React Router v5.1 版本
  3. 升级到 React Router v6 版本

以下是每个步骤的详细说明,可帮助您快速、放心地迁移到 v6。

升级到React v16.8

React Router v6 大量使用了React hooks,因此在尝试升级到 React Router v6 之前,您需要使用 React 16.8 或更高版本。 好消息是,React Router v5 与 React >= 15 兼容,因此如果您使用的是 v5(或 v4),应该能够在不触及任何路由器代码的情况下升级 React。

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

升级到React Router v5.1

如果您先升级到 v5.1,将更容易切换到 React Router v6。在 v5.1 中,我们对 <Route children> 元素的处理进行了增强,这将有助于顺利过渡到 v6。无需使用 <Route component><Route render> 属性,只需在任何地方使用常规元素 <Route children> ,并使用钩子访问路由的内部状态。

jsx
// v4 and v5 before 5.1
function User({ id }) {
  // ...
}

function App() {
  return (
    <Switch>
      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route
        path="/users/:id"
        render={({ match }) => (
          <User id={match.params.id} />
        )}
      />
    </Switch>
  );
}

// v5.1 preferred style
function User() {
  let { id } = useParams();
  // ...
}

function App() {
  return (
    <Switch>
      <Route exact path="/">
        <Home />
      </Route>
      <Route path="/about">
        <About />
      </Route>
      {/* Can also use a named `children` prop */}
      <Route path="/users/:id" children={<User />} />
    </Switch>
  );
}
// v4 and v5 before 5.1
function User({ id }) {
  // ...
}

function App() {
  return (
    <Switch>
      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route
        path="/users/:id"
        render={({ match }) => (
          <User id={match.params.id} />
        )}
      />
    </Switch>
  );
}

// v5.1 preferred style
function User() {
  let { id } = useParams();
  // ...
}

function App() {
  return (
    <Switch>
      <Route exact path="/">
        <Home />
      </Route>
      <Route path="/about">
        <About />
      </Route>
      {/* Can also use a named `children` prop */}
      <Route path="/users/:id" children={<User />} />
    </Switch>
  );
}

您可以在我们的博客上阅读有关v5.1的 hooks API 的更多信息,以及改用常规元素的理由。

一般来说,React Router v5.1(和 v6)偏爱元素而非组件(或 "元素类型")。这有几个原因,但我们将在讨论 v6 的 <Route> API 时进一步讨论。

当您使用常规的 React 元素时,您可以显式地传递道具。这有助于代码的可读性和长期维护。如果您使用 <Route render> 来获取参数,您只需在路由组件中使用 useParams 即可。

在升级到 v5.1 版的同时,您应将 withRouter 替换为钩子。您还应删除任何不在 <Switch> 中的 "浮动 " <Route> 元素。有关v5.1的博客文章再次详细介绍了如何做到这一点。

总之,要从v4/5升级到v5.1,您应该:

  • 使用<Route children>替代<Route render>和/或<Route component>属性
  • 使用我们的hooks API访问路由器状态,如当前位置和参数
  • 请将所有使用 withRouter 的地方替换为 hooks
  • 将不在 <Switch> 中的任何 <Route> 替换为 useRouteMatch ,或将其包裹在 <Switch>

移除<Switch>内的<Redirect> 元素

移除直接位于 <Switch> 内的 <Redirect> 元素。

如果要在初始渲染时重定向,则应将重定向逻辑移至服务器上(我们在此处对此进行了详细介绍)。

如果要重定向客户端,请将 <Redirect> 移至 <Route render> 属性中。

jsx
// Change this:
<Switch>
  <Redirect from="about" to="about-us" />
</Switch>

// to this:
<Switch>
  <Route path="about" render={() => <Redirect to="about-us" />} />
</Switch>
// Change this:
<Switch>
  <Redirect from="about" to="about-us" />
</Switch>

// to this:
<Switch>
  <Route path="about" render={() => <Redirect to="about-us" />} />
</Switch>

正常的 <Redirect> 元素不在 <Switch> 中是可以保留的。它们将在 v6 中变成 <Navigate> 元素。

重构自定义的 <Route> 元素

用普通的 <Route> 替换 <Switch> 中任何非纯 <Route> 元素。这包括任何 <PrivateRoute> 风格的自定义组件。

您可以在这里阅读更多相关信息,包括如何在 v5 中使用 <Route render> 属性来实现相同效果的一些技巧。

Ship it!

再次强调,一旦您的应用程序升级到 v5.1,您就应该对其进行测试和部署,并在准备好继续使用时重新拾起本指南。

升级到 React Router v6

请注意:这是迁移过程中最大的一步,可能需要花费最多的时间和精力。

在这一步中,你需要安装 React Router v6。可以通过 npm 管理依赖项:

bash
$ npm install react-router-dom
# or, for a React Native app
$ npm install react-router-native
$ npm install react-router-dom
# or, for a React Native app
$ npm install react-router-native

您还需要从 package.json 中移除 history 依赖关系。 history 库是 v6 的直接依赖库(而非对等依赖库),因此您不会直接导入或使用它。相反,您将使用 useNavigate() 钩子进行所有导航(见下文)。

将所有的 <Switch> 元素升级为 <Routes>

React Router v6 引入了一个 Routes 组件,它有点像 Switch ,但功能要强大得多。与 Switch 相比, Routes 的主要优势在于:

  • <Routes> 中的所有 <Route><Link> 都是相对的。这导致在 <Route path><Link to> 中的代码更精简和更可预测。
  • 路由的选择基于最佳匹配,而不是按顺序遍历。这避免了由于在 <Switch> 中定义较晚而导致无法到达的错误。
  • 路由可以嵌套在一个地方,而不是分散在不同的组件中。在中小型应用程序中,这样可以方便地一次性查看所有路由。在大型应用程序中,您仍然可以通过 React.lazy 动态加载将路由嵌套在打包中。

为了使用 v6,您需要将所有 <Switch> 元素转换为 <Routes> 。如果您已经升级到 v5.1,那就成功了一半。

首先,让我们来谈谈 v6 中的相对路由和链接。

相对路由和链接

在 v5 版本中,必须明确说明如何嵌套路由和链接。在这两种情况下,如果想要嵌套路由和链接,就必须从父路由的 match.urlmatch.path 属性中创建 <Route path><Link to> 属性。此外,如果要嵌套路由,就必须将它们放在子路由的组件中。

jsx
// This is a React Router v5 app
import {
  BrowserRouter,
  Switch,
  Route,
  Link,
  useRouteMatch,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/">
          <Home />
        </Route>
        <Route path="/users">
          <Users />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}

function Users() {
  // In v5, nested routes are rendered by the child component, so
  // you have <Switch> elements all over your app for nested UI.
  // You build nested routes and links using match.url and match.path.
  let match = useRouteMatch();

  return (
    <div>
      <nav>
        <Link to={`${match.url}/me`}>My Profile</Link>
      </nav>

      <Switch>
        <Route path={`${match.path}/me`}>
          <OwnUserProfile />
        </Route>
        <Route path={`${match.path}/:id`}>
          <UserProfile />
        </Route>
      </Switch>
    </div>
  );
}
// This is a React Router v5 app
import {
  BrowserRouter,
  Switch,
  Route,
  Link,
  useRouteMatch,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/">
          <Home />
        </Route>
        <Route path="/users">
          <Users />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}

function Users() {
  // In v5, nested routes are rendered by the child component, so
  // you have <Switch> elements all over your app for nested UI.
  // You build nested routes and links using match.url and match.path.
  let match = useRouteMatch();

  return (
    <div>
      <nav>
        <Link to={`${match.url}/me`}>My Profile</Link>
      </nav>

      <Switch>
        <Route path={`${match.path}/me`}>
          <OwnUserProfile />
        </Route>
        <Route path={`${match.path}/:id`}>
          <UserProfile />
        </Route>
      </Switch>
    </div>
  );
}

这与 v6 中的应用程序相同:

jsx
// This is a React Router v6 app
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="users/*" element={<Users />} />
      </Routes>
    </BrowserRouter>
  );
}

function Users() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Routes>
        <Route path=":id" element={<UserProfile />} />
        <Route path="me" element={<OwnUserProfile />} />
      </Routes>
    </div>
  );
}
// This is a React Router v6 app
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="users/*" element={<Users />} />
      </Routes>
    </BrowserRouter>
  );
}

function Users() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Routes>
        <Route path=":id" element={<UserProfile />} />
        <Route path="me" element={<OwnUserProfile />} />
      </Routes>
    </div>
  );
}

在这个例子中,需要注意 v6 的几个重要事项:

  • <Route path><Link to> 是相对的。这意味着它们会自动根据父路由的路径和 URL 建立,因此您不必手动插入 match.urlmatch.path
  • <Route exact> 已经消失了。已不复存在。取而代之的是,具有后代路由(在其他组件中定义)的路由在路径中使用尾部 * 来表示它们深度匹配。
  • 您可以按照任意顺序排列路由,路由会自动检测当前 URL 的最佳路由。这样就能避免在<Switch>中由于手动将路由顺序放错而造成的错误。

您可能还注意到,v5 应用程序中的所有 <Route children> 在 v6 中都变为了 <Route element> 。如果您按照 v5.1 版的升级步骤进行了升级,那么只需将路由元素从children 变更为 element 的属性即可。

<Route element>的优势

在有关升级到 v5.1 的章节中,我们承诺将讨论使用常规元素而非组件(或元素类型)进行渲染的优势。让我们从升级中稍作休息,现在来谈谈这个问题。

首先,我们可以看到 React 本身率先使用 <Suspense fallback={<Spinner />}> API。 fallback prop 采用的是 React 元素,而不是组件。这样,您就可以轻松地将您想要的任何属性从渲染它的组件传递到 <Spinner>

使用元素而非组件意味着我们不必提供 passProps 风格的 API,这样您就可以为元素获取所需的属性。例如,在基于组件的 API 中,没有很好的方法将属性传递给 <Profile> 元素,当 <Route path=":userId" component={Profile} /> 匹配时,该元素就会渲染。大多数采用这种方法的 React 库最终要么使用类似 <Route component={Profile} passProps={ animate: true } /> 的 API,要么使用渲染属性或高阶组件。

另外,如果你没有注意到,在 v4 和 v5 中, Route 的渲染 API 变得相当庞大。它是这样的

jsx
// 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*
// 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 高阶组件......直到hooks的出现!

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

jsx
// 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.
// 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> 保留给嵌套路由。这是 v3 和 @reach/router 中最受用户喜爱的功能之一,我们将在 v6 中恢复这一功能。将上例中的代码再向前推进一步,我们就可以将所有 <Route> 元素合并到一个路由配置中:

jsx
// This is a React Router v6 app
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  Outlet,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="users" element={<Users />}>
          <Route path="me" element={<OwnUserProfile />} />
          <Route path=":id" element={<UserProfile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Users() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Outlet />
    </div>
  );
}
// This is a React Router v6 app
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  Outlet,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="users" element={<Users />}>
          <Route path="me" element={<OwnUserProfile />} />
          <Route path=":id" element={<UserProfile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Users() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Outlet />
    </div>
  );
}

当然,这一步是可选的,但对于没有数千条路由的中小型应用程序来说,这一步真的很不错。

请注意 <Route> 元素是如何在 <Routes> 元素内部自然嵌套的。嵌套路由通过添加到父路由的路径来建立自己的路径。这次我们不需要在 <Route path="users"> 上添加尾部的 * ,因为当路由定义在一处时,路由器就能看到所有嵌套的路由。

只有在路由的后代树中存在另一个 <Routes> 时,才需要尾部的 * 。在这种情况下,后代 <Routes> 将匹配路径名的剩余部分(实际情况请参见前面的示例)。

使用嵌套配置时,带有 children 的路由应呈现 <Outlet> ,以便呈现其子路由。这样就能轻松呈现嵌套用户界面的布局。

关于 <Route path> 模式的注意事项

React Router v6 使用简化的路径格式。在 v6 中, <Route path> 只支持两种占位符:动态 :id 样式的参数和 * 通配符。 * 通配符只能在路径末尾使用,不能在中间使用。

以下所有路由路径在 v6 中都是有效的:

bash
/groups
/groups/admin
/users/:id
/users/:id/messages
/files/*
/files/:id/*
/groups
/groups/admin
/users/:id
/users/:id/messages
/files/*
/files/:id/*

以下正则表达式风格的路由路径在 v6 中无效:

bash
/users/:id?
/tweets/:id(\d+)
/files/*/cat.jpg
/files-*
/users/:id?
/tweets/:id(\d+)
/files/*/cat.jpg
/files-*

我们在 v4 中添加了 path-to-regexp 的依赖关系,以实现更高级的模式匹配。在 v6 版中,我们使用了一种更简单的语法,它允许我们为排序目的对路径进行可预测的解析。这也意味着我们可以不再依赖 path-to-regexp,这对打包的大小很有好处。

如果您正在使用 path-to-regexp 的任何高级语法,则必须将其删除并简化路由路径。如果您正在使用正则表达式语法进行 URL 参数验证(例如,确保 id 全为数字字符),请注意我们计划在第 v6 版中添加更高级的参数验证功能。目前,您需要将该逻辑移至路由渲染的组件中,并在解析参数后让它分支其渲染的树。

如果您使用的是 <Route sensitive> ,则应将其移至包含 <Routes caseSensitive> 的属性中。 <Routes> 元素中的所有路由要么区分大小写,要么不区分。

还需要注意的一点是,v6 中的所有路径匹配都会忽略 URL 上的斜线尾部。事实上, <Route strict> 已被删除,在 v6 中没有任何作用。**这并不意味着如果需要,就不能使用尾部斜线。**您的应用程序可以决定是否使用尾随斜线,只是不能在 <Route path="edit"><Route path="edit/"> 上呈现两个不同的客户端用户界面。您仍然可以在这些 URL 上呈现两个不同的用户界面(尽管我们不建议这样做),但您必须在服务器端这样做。

在 v5 版中,不以 / 开头的 <Link to> 值是模棱两可的;它取决于当前 URL 是什么。例如,如果当前 URL 是 /users ,则 v5 版 <Link to="me"> 将显示 <a href="/me"> 。但是,如果当前 URL 带有尾部斜线,如 /users/ ,则同样的 <Link to="me"> 将显示 <a href="/users/me"> 。这样就很难预测链接的行为,因此在 v5 版中,我们建议您从根 URL(使用 match.url )建立链接,而不要使用相对的 <Link to> 值。

React Router v6 版本修正了这一模糊之处。在 v6 中,无论当前 URL 如何, <Link to="me"> 都会呈现相同的 <a href>

例如,在 <Route path="users"> 中呈现的 <Link to="me"> 将始终呈现指向 /users/me 的链接,无论当前 URL 是否带有尾部斜线。

当您想 "向上 "链接父路由时,请在 <Link to> 值中使用一个前导 .. 段,类似于 <a href> 中的做法。

jsx
function App() {
  return (
    <Routes>
      <Route path="users" element={<Users />}>
        <Route path=":id" element={<UserProfile />} />
      </Route>
    </Routes>
  );
}

function Users() {
  return (
    <div>
      <h2>
        {/* This links to /users - the current route */}
        <Link to=".">Users</Link>
      </h2>

      <ul>
        {users.map((user) => (
          <li>
            {/* This links to /users/:id - the child route */}
            <Link to={user.id}>{user.name}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

function UserProfile() {
  return (
    <div>
      <h2>
        {/* This links to /users - the parent route */}
        <Link to="..">All Users</Link>
      </h2>

      <h2>
        {/* This links to /users/:id - the current route */}
        <Link to=".">User Profile</Link>
      </h2>

      <h2>
        {/* This links to /users/mj - a "sibling" route */}
        <Link to="../mj">MJ</Link>
      </h2>
    </div>
  );
}
function App() {
  return (
    <Routes>
      <Route path="users" element={<Users />}>
        <Route path=":id" element={<UserProfile />} />
      </Route>
    </Routes>
  );
}

function Users() {
  return (
    <div>
      <h2>
        {/* This links to /users - the current route */}
        <Link to=".">Users</Link>
      </h2>

      <ul>
        {users.map((user) => (
          <li>
            {/* This links to /users/:id - the child route */}
            <Link to={user.id}>{user.name}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

function UserProfile() {
  return (
    <div>
      <h2>
        {/* This links to /users - the parent route */}
        <Link to="..">All Users</Link>
      </h2>

      <h2>
        {/* This links to /users/:id - the current route */}
        <Link to=".">User Profile</Link>
      </h2>

      <h2>
        {/* This links to /users/mj - a "sibling" route */}
        <Link to="../mj">MJ</Link>
      </h2>
    </div>
  );
}

将当前的 URL 想象成文件系统上的目录路径,而 <Link to> 就像命令行工具中的 cd 命令。

bash
// If your routes look like this
<Route path="app">
  <Route path="dashboard">
    <Route path="stats" />
  </Route>
</Route>

// and the current URL is /app/dashboard (with or without
// a trailing slash)
<Link to="stats">               => <a href="/app/dashboard/stats">
<Link to="../stats">            => <a href="/app/stats">
<Link to="../../stats">         => <a href="/stats">
<Link to="../../../stats">      => <a href="/stats">

// On the command line, if the current directory is /app/dashboard
cd stats                        # pwd is /app/dashboard/stats
cd ../stats                     # pwd is /app/stats
cd ../../stats                  # pwd is /stats
cd ../../../stats               # pwd is /stats
// If your routes look like this
<Route path="app">
  <Route path="dashboard">
    <Route path="stats" />
  </Route>
</Route>

// and the current URL is /app/dashboard (with or without
// a trailing slash)
<Link to="stats">               => <a href="/app/dashboard/stats">
<Link to="../stats">            => <a href="/app/stats">
<Link to="../../stats">         => <a href="/stats">
<Link to="../../../stats">      => <a href="/stats">

// On the command line, if the current directory is /app/dashboard
cd stats                        # pwd is /app/dashboard/stats
cd ../stats                     # pwd is /app/stats
cd ../../stats                  # pwd is /stats
cd ../../../stats               # pwd is /stats

注意:在匹配和创建相对路径时忽略尾部斜线的决定不是我们团队轻易做出的。我们就此咨询了许多朋友和客户(他们也是我们的朋友!)。我们发现,大多数人甚至不了解普通 HTML 相对链接是如何处理尾部斜线的。大多数人猜测它的工作原理类似于命令行上的 cd (事实并非如此)。此外,HTML 相对链接没有嵌套路由的概念,它们只适用于 URL,因此我们必须自己开辟一条新道路。 @reach/router 开创了这一先例,几年来一直运行良好。

除了忽略当前 URL 中的尾部斜线外,需要注意的是,当 <Route path> 与多个 URL 片段匹配时, <Link to=".."> 的行为并不总是与 <a href=".."> 相同。它会根据父路由的路径进行解析,而不是只删除 URL 中的一个段落,基本上会删除该路由指定的所有路径段。

jsx
function App() {
  return (
    <Routes>
      <Route path="users">
        <Route
          path=":id/messages"
          element={
            // This links to /users
            <Link to=".." />
          }
        />
      </Route>
    </Routes>
  );
}
function App() {
  return (
    <Routes>
      <Route path="users">
        <Route
          path=":id/messages"
          element={
            // This links to /users
            <Link to=".." />
          }
        />
      </Route>
    </Routes>
  );
}

.. 在路由而非 URL 片段上运行,这似乎是一个奇怪的选择,但在处理 * 路由时,它却能提供巨大的帮助,因为 * 可能会匹配不确定数量的片段。在这些情况下, <Link to> 值中的单个 .. 片段基本上可以移除 * 匹配的任何内容,这样就可以在 * 路由中创建更可预测的链接。

jsx
function App() {
  return (
    <Routes>
      <Route path=":userId">
        <Route path="messages" element={<UserMessages />} />
        <Route
          path="files/*"
          element={
            // This links to /:userId/messages, no matter
            // how many segments were matched by the *
            <Link to="../messages" />
          }
        />
      </Route>
    </Routes>
  );
}
function App() {
  return (
    <Routes>
      <Route path=":userId">
        <Route path="messages" element={<UserMessages />} />
        <Route
          path="files/*"
          element={
            // This links to /:userId/messages, no matter
            // how many segments were matched by the *
            <Link to="../messages" />
          }
        />
      </Route>
    </Routes>
  );
}

在 v6 中, Link 组件将 state 作为单独的属性接收,而不是作为传递给 to 的对象的一部分接收,因此,如果 Link 组件使用 state ,则需要更新这些组件:

jsx
import { Link } from "react-router-dom";

// Change this:
<Link to={{ pathname: "/home", state: state }} />

// to this:
<Link to="/home" state={state} />
import { Link } from "react-router-dom";

// Change this:
<Link to={{ pathname: "/home", state: state }} />

// to this:
<Link to="/home" state={state} />

状态值仍然通过 useLocation() 在链接组件中检索:

jsx
function Home() {
  const location = useLocation();
  const state = location.state;
  return <div>Home</div>;
}
function Home() {
  const location = useLocation();
  const state = location.state;
  return <div>Home</div>;
}

使用useRoutes代替react-router-config

v5 版本的 react-router-config 包中的所有功能都已移至 v6 的核心中。如果您喜欢/需要将路由定义为 JavaScript 对象,而不是使用 React 元素,那么您一定会喜欢这个功能。

jsx
function App() {
  let element = useRoutes([
    // These are the same as the props you provide to <Route>
    { path: "/", element: <Home /> },
    { path: "dashboard", element: <Dashboard /> },
    {
      path: "invoices",
      element: <Invoices />,
      // Nested routes use a children property, which is also
      // the same as <Route>
      children: [
        { path: ":id", element: <Invoice /> },
        { path: "sent", element: <SentInvoices /> },
      ],
    },
    // Not found routes work as you'd expect
    { path: "*", element: <NotFound /> },
  ]);

  // The returned element will render the entire element
  // hierarchy with all the appropriate context it needs
  return element;
}
function App() {
  let element = useRoutes([
    // These are the same as the props you provide to <Route>
    { path: "/", element: <Home /> },
    { path: "dashboard", element: <Dashboard /> },
    {
      path: "invoices",
      element: <Invoices />,
      // Nested routes use a children property, which is also
      // the same as <Route>
      children: [
        { path: ":id", element: <Invoice /> },
        { path: "sent", element: <SentInvoices /> },
      ],
    },
    // Not found routes work as you'd expect
    { path: "*", element: <NotFound /> },
  ]);

  // The returned element will render the entire element
  // hierarchy with all the appropriate context it needs
  return element;
}

以这种方式定义的路由与 <Routes> 的语义相同。实际上, <Routes> 只是 useRoutes 的一个包装。

我们鼓励您同时试用 <Routes>useRoutes ,然后自己决定更喜欢使用哪一个。老实说,我们两个都喜欢,也都在使用。

如果你已经围绕数据获取和服务器端渲染设计了一些自己的逻辑,我们还提供了一个底层 matchRoutes 函数,与 react-router-config 中的函数类似。

使用useNavigate代替useHistory

React Router v6 引入了新的导航 API,该 API 与 <Link> 同义,可更好地兼容启用了悬念的应用程序。根据您的风格和需求,我们提供了该 API 的命令式和声明式版本。

jsx
// This is a React Router v5 app
import { useHistory } from "react-router-dom";

function App() {
  let history = useHistory();
  function handleClick() {
    history.push("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}
// This is a React Router v5 app
import { useHistory } from "react-router-dom";

function App() {
  let history = useHistory();
  function handleClick() {
    history.push("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}

在 v6 版本中,应重写此应用程序以使用 navigate API。大多数情况下,这意味着要将 useHistory 更改为 useNavigate ,并更改 history.pushhistory.replace 调用站点。

jsx
// This is a React Router v6 app
import { useNavigate } from "react-router-dom";

function App() {
  let navigate = useNavigate();
  function handleClick() {
    navigate("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}
// This is a React Router v6 app
import { useNavigate } from "react-router-dom";

function App() {
  let navigate = useNavigate();
  function handleClick() {
    navigate("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}

如果需要替换当前位置而不是向历史记录堆栈推送新位置,请使用 navigate(to, { replace: true }) 。如果需要状态,请使用 navigate(to, { state }) 。可以将 navigate 的第一个参数视为 <Link to> ,其他参数视为 replacestate

如果您喜欢使用声明式 API 进行导航(如 v5 的 Redirect 组件),v6 提供了 Navigate 组件。使用方法如下:

jsx
import { Navigate } from "react-router-dom";

function App() {
  return <Navigate to="/home" replace state={state} />;
}
import { Navigate } from "react-router-dom";

function App() {
  return <Navigate to="/home" replace state={state} />;
}

注意:请注意,v5 版 <Redirect /> 默认使用 replace 逻辑(可通过 push 属性进行更改),而 v6 版 <Navigate /> 默认使用 push 逻辑,可通过 replace 属性进行更改。

jsx
// Change this:
<Redirect to="about" />
<Redirect to="home" push />

// to this:
<Navigate to="about" replace />
<Navigate to="home" />
// Change this:
<Redirect to="about" />
<Redirect to="home" push />

// to this:
<Navigate to="about" replace />
<Navigate to="home" />

如果您当前正在使用 useHistory 中的 gogoBackgoForward 来前后导航,则还应该将其替换为 navigate ,并在其中加入一个数字参数,指示指针在历史记录堆栈中的位置。例如,下面是一些使用 v5 的 useHistory 钩子的代码:

jsx
// This is a React Router v5 app
import { useHistory } from "react-router-dom";

function App() {
  const { go, goBack, goForward } = useHistory();

  return (
    <>
      <button onClick={() => go(-2)}>
        Go 2 pages back
      </button>
      <button onClick={goBack}>Go back</button>
      <button onClick={goForward}>Go forward</button>
      <button onClick={() => go(2)}>
        Go 2 pages forward
      </button>
    </>
  );
}
// This is a React Router v5 app
import { useHistory } from "react-router-dom";

function App() {
  const { go, goBack, goForward } = useHistory();

  return (
    <>
      <button onClick={() => go(-2)}>
        Go 2 pages back
      </button>
      <button onClick={goBack}>Go back</button>
      <button onClick={goForward}>Go forward</button>
      <button onClick={() => go(2)}>
        Go 2 pages forward
      </button>
    </>
  );
}

下面是与 v6 版本相当的应用程序:

jsx
// This is a React Router v6 app
import { useNavigate } from "react-router-dom";

function App() {
  const navigate = useNavigate();

  return (
    <>
      <button onClick={() => navigate(-2)}>
        Go 2 pages back
      </button>
      <button onClick={() => navigate(-1)}>Go back</button>
      <button onClick={() => navigate(1)}>
        Go forward
      </button>
      <button onClick={() => navigate(2)}>
        Go 2 pages forward
      </button>
    </>
  );
}
// This is a React Router v6 app
import { useNavigate } from "react-router-dom";

function App() {
  const navigate = useNavigate();

  return (
    <>
      <button onClick={() => navigate(-2)}>
        Go 2 pages back
      </button>
      <button onClick={() => navigate(-1)}>Go back</button>
      <button onClick={() => navigate(1)}>
        Go forward
      </button>
      <button onClick={() => navigate(2)}>
        Go 2 pages forward
      </button>
    </>
  );
}

同样,我们从直接使用 history API 转向使用 navigate API 的主要原因之一是为了更好地兼容 React suspense。React Router v6 在组件层次结构的根部使用 useNavigation 钩子。这让我们能够在用户交互需要中断待定路由导航时提供更流畅的体验,例如,当用户点击指向另一个路由的链接时,之前点击的链接仍在加载中。 navigate API 知道内部的待定导航状态,因此会在历史记录堆栈中进行 REPLACE 而不是 PUSH,这样用户的历史记录中就不会出现从未加载过的页面。

注意:v5中的 <Redirect> 元素不再支持作为路由配置的一部分(在 <Routes> 中)。这是因为 React 中即将出现的更改会导致在初始呈现期间更改路由器的状态变得不安全。如果您需要立即重定向,可以 a) 在服务器上完成(可能是最好的解决方案),或者 b) 在路由组件中呈现 <Navigate> 元素。不过,请注意导航将在 useEffect 中进行。

除了支持 Suspense 之外, navigateLink 一样,支持相对导航。例如:

jsx
// assuming we are at `/stuff`
function SomeForm() {
  let navigate = useNavigate();
  return (
    <form
      onSubmit={async (event) => {
        let newRecord = await saveDataFromForm(
          event.target
        );
        // you can build up the URL yourself
        navigate(`/stuff/${newRecord.id}`);
        // or navigate relative, just like Link
        navigate(`${newRecord.id}`);
      }}
    >
      {/* ... */}
    </form>
  );
}
// assuming we are at `/stuff`
function SomeForm() {
  let navigate = useNavigate();
  return (
    <form
      onSubmit={async (event) => {
        let newRecord = await saveDataFromForm(
          event.target
        );
        // you can build up the URL yourself
        navigate(`/stuff/${newRecord.id}`);
        // or navigate relative, just like Link
        navigate(`${newRecord.id}`);
      }}
    >
      {/* ... */}
    </form>
  );
}

<Link> 不再支持覆盖返回的锚点标记的 component 属性。这有几个原因。

首先, <Link> 几乎总是应该呈现 <a> 。如果您的应用程序不能这样做,那么您的应用程序很有可能存在严重的可访问性和可用性问题,这可不是什么好事。浏览器通过 <a> 为我们提供了很多很好的可用性功能,我们希望您的用户能免费获得这些功能!

尽管如此,也许您的应用程序使用的是 CSS-in-JS 库,也许您的设计系统中已经有了一个自定义的、花哨的链接组件,而您希望用它来代替。在没有钩子的世界里, component 工具可能已经足够好用了,但现在您只需使用我们的几个钩子,就能创建自己的可访问 Link 组件:

jsx
import { FancyPantsLink } from "@fancy-pants/design-system";
import {
  useHref,
  useLinkClickHandler,
} from "react-router-dom";

const Link = React.forwardRef(
  (
    {
      onClick,
      replace = false,
      state,
      target,
      to,
      ...rest
    },
    ref
  ) => {
    let href = useHref(to);
    let handleClick = useLinkClickHandler(to, {
      replace,
      state,
      target,
    });

    return (
      <FancyPantsLink
        {...rest}
        href={href}
        onClick={(event) => {
          onClick?.(event);
          if (!event.defaultPrevented) {
            handleClick(event);
          }
        }}
        ref={ref}
        target={target}
      />
    );
  }
);
import { FancyPantsLink } from "@fancy-pants/design-system";
import {
  useHref,
  useLinkClickHandler,
} from "react-router-dom";

const Link = React.forwardRef(
  (
    {
      onClick,
      replace = false,
      state,
      target,
      to,
      ...rest
    },
    ref
  ) => {
    let href = useHref(to);
    let handleClick = useLinkClickHandler(to, {
      replace,
      state,
      target,
    });

    return (
      <FancyPantsLink
        {...rest}
        href={href}
        onClick={(event) => {
          onClick?.(event);
          if (!event.defaultPrevented) {
            handleClick(event);
          }
        }}
        ref={ref}
        target={target}
      />
    );
  }
);

如果你正在使用 react-router-native ,我们提供的 useLinkPressHandler 工作方式基本相同。只需在 LinkonPress 处理程序中调用该钩子返回的函数,就可以了。

这是一个简单的属性重命名,以便更好地与 React 生态系统中其他库的常见做法保持一致。

v6.0.0-beta.3 开始, activeClassNameactiveStyle 的属性已从 NavLinkProps 中移除。取而代之的是,您可以向 styleclassName 传递一个函数,从而根据组件的活动状态自定义内联样式或类字符串。

jsx
<NavLink
  to="/messages"
- style={{ color: 'blue' }}
- activeStyle={{ color: 'green' }}
+ style={({ isActive }) => ({ color: isActive ? 'green' : 'blue' })}
>
  Messages
</NavLink>
<NavLink
  to="/messages"
- style={{ color: 'blue' }}
- activeStyle={{ color: 'green' }}
+ style={({ isActive }) => ({ color: isActive ? 'green' : 'blue' })}
>
  Messages
</NavLink>
jsx
<NavLink
  to="/messages"
- className="nav-link"
- activeClassName="activated"
+ className={({ isActive }) => "nav-link" + (isActive ? " activated" : "")}
>
  Messages
</NavLink>
<NavLink
  to="/messages"
- className="nav-link"
- activeClassName="activated"
+ className={({ isActive }) => "nav-link" + (isActive ? " activated" : "")}
>
  Messages
</NavLink>

如果您希望保留 v5 版本的属性,可以创建自己的 <NavLink /> 作为封装组件,以实现更顺畅的升级路径。

jsx
import * as React from "react";
import { NavLink as BaseNavLink } from "react-router-dom";

const NavLink = React.forwardRef(
  ({ activeClassName, activeStyle, ...props }, ref) => {
    return (
      <BaseNavLink
        ref={ref}
        {...props}
        className={({ isActive }) =>
          [
            props.className,
            isActive ? activeClassName : null,
          ]
            .filter(Boolean)
            .join(" ")
        }
        style={({ isActive }) => ({
          ...props.style,
          ...(isActive ? activeStyle : null),
        })}
      />
    );
  }
);
import * as React from "react";
import { NavLink as BaseNavLink } from "react-router-dom";

const NavLink = React.forwardRef(
  ({ activeClassName, activeStyle, ...props }, ref) => {
    return (
      <BaseNavLink
        ref={ref}
        {...props}
        className={({ isActive }) =>
          [
            props.className,
            isActive ? activeClassName : null,
          ]
            .filter(Boolean)
            .join(" ")
        }
        style={({ isActive }) => ({
          ...props.style,
          ...(isActive ? activeStyle : null),
        })}
      />
    );
  }
);

react-router-dom/server中获取StaticRouter

StaticRouter 组件已迁移到新的包中: react-router-dom/server

jsx
// change
import { StaticRouter } from "react-router-dom";
// to
import { StaticRouter } from "react-router-dom/server";
// change
import { StaticRouter } from "react-router-dom";
// to
import { StaticRouter } from "react-router-dom/server";

这一改动既是为了更紧密地遵循 react-dom 软件包所建立的惯例,也是为了帮助用户更好地理解 <StaticRouter> 的用途和以及何时应该使用它(在服务器上)。

使用useMatch替换useRouteMatch

useMatch 与v5的 useRouteMatch 非常相似,但有一些主要的区别:

  • 它使用我们的新路径模式匹配算法
  • 现在需要模式参数
  • 不再接受模式数组
  • 以对象形式传递模式时,某些选项已重新命名,以便更好地与 v6 版中的其他 API 保持一致
    • useRouteMatch({ strict }) 现在是 useMatch({ end })
    • useRouteMatch({ sensitive }) 现在是 useMatch({ caseSensitive })
  • 它会返回一个形状不同的匹配对象

要查看新 useMatch 钩子的准确 API 及其类型声明,请查阅我们的 API 参考文档

更改传递给 matchPath 的参数顺序,更改 pathPattern 选项

从第 6 版开始,传递给 matchPath 函数的参数顺序发生了变化。模式选项也发生了变化。

  • 第一个参数是 pathPattern 对象,然后是 pathname
  • pathPattern 不再包括 exactstrict 选项。新增了 caseSensitiveend 选项

请按以下方式重构:

Before:

jsx
// This is a React Router v5 app
import { matchPath } from "react-router-dom";

const match = matchPath("/users/123", {
  path: "/users/:id",
  exact: true, // Optional, defaults to false
  strict: false, // Optional, defaults to false
});
// This is a React Router v5 app
import { matchPath } from "react-router-dom";

const match = matchPath("/users/123", {
  path: "/users/:id",
  exact: true, // Optional, defaults to false
  strict: false, // Optional, defaults to false
});

After:

jsx
// This is a React Router v6 app
import { matchPath } from "react-router-dom";

const match = matchPath(
  {
    path: "/users/:id",
    caseSensitive: false, // Optional, `true` == static parts of `path` should match case
    end: true, // Optional, `true` == pattern should match the entire URL pathname
  },
  "/users/123"
);
// This is a React Router v6 app
import { matchPath } from "react-router-dom";

const match = matchPath(
  {
    path: "/users/:id",
    caseSensitive: false, // Optional, `true` == static parts of `path` should match case
    end: true, // Optional, `true` == pattern should match the entire URL pathname
  },
  "/users/123"
);

<Prompt> 目前不受支持

v5 版本的<Prompt>(以及 v6 beta版的 usePromptuseBlocker)不存在当前发布的 v6 版本中。我们决定,与其花更多的时间去完善一个尚未完全成熟的功能,还不如利用现有的功能来发布新版本。在不久的将来,我们一定会在 v6 中重新加入这一功能,但不会在 6.x 的第一个稳定版本中加入。

我们错过了什么?

尽管我们尽力做到详尽无遗,但还是很可能有遗漏。如果您按照本升级指南进行了升级,发现确实存在这种情况,请告诉我们。我们很乐意帮助您了解如何处理 v5 代码,以便升级并使用 v6 中的所有功能。

祝你好运 🤘