主線
分支
主線 (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 的慣用語法(也稱為 hooks)。
  • 提供更好的即時模式和 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 中的所有掛勾都相同(或幾乎相同)

大多數變更只有一些重新命名。如果您碰巧寫了一個 codemod,請與我們分享,我們會將它新增到本指南中!

升級概觀

在本指南中,我們將向您展示如何升級路徑代碼中的每個部分。我們將逐步進行,以便您可以在方便的時候進行一些變更、配送,然後再回來進行遷移。我們還會稍微討論一下「為什麼」進行這些變更,看起來像是一個簡單的重新命名,其實背後有更大的原因。

首先:非中斷更新

我們強烈建議您在遷移到 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

更新路由元件以使用 Hook

你可以一次處理一個路由元件,然後提交並佈署。你不需要一次更新整個應用程式。

@reach/router v1.3 中,我們新增了在準備 React Router v6 時可存取路由資料的 Hook。如果你先執行此動作,在升級到 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();
  // ...
}

理由

這些資料都已經存在於 context 中,但從應用程式程式碼中存取這些資料很麻煩,因此我們將這些資料建置到 props 中。Hook 簡化了從 context 存取資料的方式,因此我們不再需要透過路由資訊來污染 props。

不污染 props 對 TypeScript 也有點幫助,還可以避免你在檢視元件時想知道道具來自哪裡。如果你正在使用來自路由器的資料,現在完全清楚了。

此外,當頁面變大時,你會自然地將其拆分為多個元件,並最終將資料「道具串接」到樹狀結構的最底層。現在,你可以在樹狀結構中的任何位置存取路由資料。它不僅更方便,而且可以建立以路由器為中心的可組合抽象化。如果自訂 Hook 需要位置,它現在可以透過 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 的工作。你的動態網路伺服器或靜態檔案伺服器應該處理這項工作,並傳送適當的回應狀態碼(例如 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
      }
    ]
  }
}

無論我們是使用無伺服器函式進行伺服器渲染,還是只將其用作靜態檔案伺服器,這種方法都適用。所有網路託管服務都會提供設定此功能的方式。

如果你的應用程式中仍然存在 <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,人們將更有可能正確地設定重新導向。多年來,我們一直意外鼓勵不良做法,希望可以停止 🙈。

這個 prop 取得器可用於將連結設定為「active」樣式。決定連結是否為 active 有點主觀。有時你希望它在 URL 完全符合時才為 active,有時你希望它在部分符合時為 active,甚至還有涉及搜尋參數和位置狀態的更多邊緣案例。

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

理由

「prop 取得器」很笨重,而且幾乎都可以用鉤子取代。這也允許你使用其他鉤子,例如 useLocation,並執行更多自訂動作,例如讓連結使用搜尋字串為 active 狀態

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/> 組件。它使用 render prop 來撰寫行為,但我們現在有鉤子了。

如果您喜歡,或只是不想更新程式碼,回朔並不難

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

理由說明

現在我們有掛勾了,呈現屬性變得很噁心(噁!)。

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

請提供意見反應

請讓我們知道這個指南是否有幫助

建立 Pull Request:請添加我們遺漏且您需要的任何遷移。

一般意見反應:推特 @remix_run,或傳送電子郵件至 hello@remix.run

謝謝!