Sean's Blog

An image showing avatar

Hi, I'm Sean

這裡記錄我學習網站開發的筆記
歡迎交流 (ゝ∀・)b

LinkedInGitHub

升級 React Router v6 筆記

本文為 React Router 升級第 6 版的筆記。

React Router v6 Changelog

  1. Switch 替換成 Routes
    • RouteRoutes 包起來
    • 不再需要 exact 屬性
  2. 調整 Nested Routes 定義方式
    • 加上米字號來配對子元件中的 Routes
    • 巢狀路由的子元件的路徑寫法
    • 集中定義 Routes 搭配 Outlet Component,且無需米字號
  3. NavLinkactiveClassName 移除,請改用 className
  4. Redirect 替換成 Navigate
  5. 移除 useHistory 改用 useNavigate 執行程式化導航
  6. Prompt 直接移除,沒有替代方案
  7. 提供 Suspense 元件

1. Switch 改名為 Routes 並改用 element 屬性指定渲染元件

第一個變化,就是 React Router v6 將原本的 <Switch> 改名為 <Routes>。只是單純更改名稱,用法沒有改變,在使用上一樣是要用 <BrowserRouter> 包住 <App> 去指定是要在 <App> 底下規劃路由。

然而,註冊路由元件的方式就不一樣了,原先 v5 我們是在 <Route> 的子層放入 Page Component,更新到 v6 後則是改用 <Route> 元件的 element 屬性來指定要渲染的頁面元件等 JSX 程式碼。

把 Route 用 Routes 包起來

錯誤訊息:A <Route> is only ever to be used as the child of <Routes> element, never rendered directly. Please wrap your <Route> in a <Routes>.

在 v5,我們不一定要使用 <Switch> 來包住 <Route>。但是到了 React Router v6 就強制大家都要在 <Route> 外面包一層 <Routes>,否則會出現以上錯誤訊息。

不再需要 exact 屬性

在 v5 我們需要使用 exact 確保配對完全符合的路徑,才不會說只要前半段符合就通通出現。但是到了 v6 我們不再需要加上 exact,因為 React Router v6 已經預設讓所有的 <Route> 都套用 exact 的效果了。

1<Routes>
2  <Route path="/welcome" element={<Welcome />} />
3  <Route path="/products" element={<Products />} />
4  <Route path="/products/:productId" element={<ProductDetail />} />
5</Routes>
6

這麼做的好處是,我們不用再去注意註冊路由時的「順序」。之前在 v5 如果有使用到動態路由,我們需要記得把動態路由放到較後面的順序註冊,因為如果底下還有其他路徑(像是 /products/edit),Router 會永遠無法進入那一頁,因為它會被上面的動態路徑 /products/:productId 攔截。

升級到 v6 後,React Router 會幫我們自動匹配最佳的路由。

2. 調整 Nested Routes 定義方式

使用米字號配對子元件中的 Routes

v6 docs: Routes with descendant routes (defined in other components) use a trailing * in their path to indicate they match deeply.

如果你有巢狀路由,例如 /welcome/welcome/new-user,那麼它們都會顯示 <Welcome> 這個 Page Component,因為是巢狀的關係。

在 React Router v6,如果子元件裡面有配置路由(<Routes><Route /></Routes>),我們就必須使用米字號(/welcome/*)讓 Router 可以訪問到子路由,而不是只能與 /welcome 這一個路由配對。

1<Route path="/welcome/*" element={<Welcome />} />
2

下面會介紹另一個「集中管理」的方式是不需要加上米字號的。

子元件的路徑寫法

v6 巢狀路由不用再撰寫上層的路徑,因為 v6 會幫我們自動判斷並加入,所以 /welcome/* 底下的巢狀子路由,就可以將 /welcome/new-user 直接寫成 new-user。如果使用 <Link> 也是一樣,可以省略前面的路由。

但要特別注意,如果要這樣寫,最前方「不能加上斜線」,否則會直接成為第一層路由。

1const Welcome = () => {
2  return (
3    <section>
4      <h1>The Welcome Page</h1>
5      <Link to="new-user">New User</Link>
6      <Routes>
7        <Route path="new-user" element={<p>Welcome, new user!</p>} />
8      </Routes>
9    </section>
10  );
11};
12

集中定義:Routes 與 Outlet 元件 (Recommended)

React Router v6 全新支援將 Nested Routes 集中於 App.js 中定義,讓我們更方便地管理巢狀路由,而非分散在各個元件當中。

另外,使用這個定義方式的話,父層的 Route 可以不用加上米字號!

v6 docs:
Notice how <Route> elements nest naturally inside a <Routes> element. Nested routes build their path by adding to the parent route's path. We didn't need a trailing * on <Route path="users"> this time because when the routes are defined in one spot the router is able to see all your nested routes.

You'll only need the trailing * when there is another <Routes> somewhere in that route's descendant tree. In that case, the descendant <Routes> will match on the portion of the pathname that remains (see the previous example for what this looks like in practice).

1<Route path="/welcome" element={<Welcome />}>
2  <Route path="new-user" element={<p>Welcome, new user!</p>} />
3</Route>
4

路由定義完成後,我們需要去指定子路由要渲染的 JSX 要放在哪邊。我們使用 v6 新增的 <Outlet> 元件,它的作用是一個 Placeholder,用來表示要在哪裡插入 JSX 內容。

1const Welcome = () => {
2  return (
3    <section>
4      <h1>The Welcome Page</h1>
5      <Link to="new-user">New User</Link>
6      <Outlet />
7    </section>
8  );
9};
10

以上集中定義的做法並不是強制的,但是個人推薦這樣處理更好理解 👍

3. 移除 NavLink 的 activeClassName 屬性,請改用 className 實作

在 v5 時,我們想要加上 active 的 Class 可能會這樣添加,但是現在這個屬性直接被移除了。

1<NavLink activeClassName={classes.active} to="/welcome">
2  Welcome
3</NavLink>
4

取而代之的是在 className 用一個特殊的方式來實作,v6 開放在 className 放一個函式!

這個函式有一個參數 navData,它是一個物件,裡面有一個名為 isActive 的屬性,這個屬性在該 NavLink 正被造訪、處於活動狀態 (Active) 時,其值就會是 true

那麼該如何使用它呢?我們透過箭頭函式 Return Value 的特性,動態判斷 navData.isActive 的值,若為 true 就回傳 classes.active,若為 false 則回傳空字串。

1<NavLink
2  className={(navData) => (navData.isActive ? classes.active : '')}
3  to="/welcome"
4>
5  Welcome
6</NavLink>
7

4. Redirect 改名為 Navigate 且新增 replace 方法

v5 我們使用 <Redirect> 進行重新導向。

1<Switch>
2  <Route path="/" exact>
3    <Redirect to="/welcome" />
4  </Route>
5</Switch>
6

v6 則更換成 <Navigate> 元件。還可以加上 replace 屬性,讓重新導向時使用「取代」的方式,如果拿掉沒寫就會是預設「Push」的導向方式。

1<Routes>
2  <Route path="/" element={<Navigate replace to="/welcome" />} />
3  <Route path="/welcome" element={<Welcome />} />
4  <Route path="/products" element={<Products />} />
5  <Route path="/products/:productId" element={<ProductDetail />} />
6</Routes>
7

5. 移除 useHistory 改用 useNavigate 執行程式化導航

v6 把 useHistory 移除了,取而代之的是 useNavigate。它一樣擁有 Push、Replace、前後導向等功能,但是在寫法上看起來更簡單了。

1const navigate = useNavigate();
2navigate('/welcome');
3
4// 如果要用 Redirect 也就是 Replace 的話,可以加上第二個參數
5navigate('/welcome', { replace: true });
6
7// 單純加上 -1,表示回到上一頁
8navigate(-1);
9
10// 回到上上一頁
11navigate(-2);
12
13// 進到下一頁
14navigate(1);
15

除此之外,useNavigate 同樣也能支援物件形式,可以處理比較複雜的路徑。

甚至可以使用 createSearchParams 幫我們自動將 Params 物件轉為 URL 字串!使用時要注意 search 的值前面要加上一個 ? 作為 Query String 的開頭喔。

1// 支援物件形式
2navigate({
3  pathname: `${location.pathname}`,
4  search: `?sort=${isSortingAscending ? 'desc' : 'asc'}`,
5});
6
7// createSearchParams
8const params = { sort: isSortingAscending ? 'desc' : 'asc' };
9navigate({
10  pathname: location.pathname,
11  search: `?${createSearchParams(params)}`,
12});
13

6. Prompt 直接移除,沒有替代方案

這個是缺點,目前還沒有替代方案,你各位只能自己想辦法啊 🥲

7. Optimize code with Lazy Loading and Suspense fallback

  • React.memo
  • Lazy Loading - Load code only when it's needed

進入頁面前,Browser 會載入我們打包的整個 Bundle,涵蓋所有的 React Code,讓我們進入後可以看到渲染的頁面,以及 Reactive 的各種狀態。

這也代表著進入頁面的 Users 必須等待下載 Code 的過程,直到我們的 Web App 準備完畢為止。

因此,我們想要做的是儘量減少 Users 初次載入頁面的等待時間,透過把大包的 Bundle 切分成一小包一小包 (chunk) 的方式,並且只有在訪問到該頁面時,才去下載該頁面的 Code。

例如:初始進入的頁面是 All Quotes 頁面 (our-domain.com/quotes),此時就不需要載入 New Quote 頁面 (our-domain.com/new-quote) 的程式碼。

1// import NewQuote from './pages/NewQuote';
2const NewQuote = React.lazy(() => import('./pages/NewQuote'));
3

完成後回到頁面,會發現頁面顯示有錯誤。這是因為我們把檔案拆分成 chunks 之後,如我們所願 React Router 進行了延遲加載,但是 React 卻也因此無法順利進行渲染的工作而導致 React 報錯。

為了讓 React 繼續進行渲染而非報錯,我們可以準備一個替代的元件 <Suspense> 給它,讓 React 在載入完成前先渲染這個替代內容。其中的替代內容我們就寫在 fallback 這個屬性裡面,透過箭頭函式回傳 JSX 內容。

範例:將 Lazy Loading 應用在各個需要的頁面上

1const AllQuotes = React.lazy(() => import('./pages/AllQuotes'));
2const NewQuote = React.lazy(() => import('./pages/NewQuote'));
3const QuoteDetail = React.lazy(() => import('./pages/QuoteDetail'));
4const NotFound = React.lazy(() => import('./pages/NotFound'));
5
6function App() {
7  return (
8    <div>
9      <Layout>
10        <Suspense
11          fallback={
12            <div className="centered">
13              <LoadingSpinner />
14            </div>
15          }
16        >
17          <Routes>
18            <Route path="/quotes" element={<AllQuotes />} />
19            <Route path="/new-quote" element={<NewQuote />} />
20            <Route path="/quotes/:quoteId/*" element={<QuoteDetail />} />
21            <Route path="*" element={<NotFound />} />
22          </Routes>
23        </Suspense>
24      </Layout>
25    </div>
26  );
27}
28

Recap

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…

  • Upgrading To React Router v6
  • Optimize code with Lazy Loading and Suspense fallback

References