【React】React router (v6)
在學 React 的時候,一定會接觸到 React router 來建立路由。在沒有 Next.js 的 Page router 及 App router 以前,React router 是最常用的路由套件。在去年 2022/09/14 發佈了 v6.4 版本,從這個版本開始,新增了很多新功能,主要集中在 API fetching、Data loading 等功能上,而要使用這些功能必須把原本的 BrowserRouter 換成 createBrowserRouter API 及 <RouterProvider/> 元件,這一篇就來介紹 v6 的新功能及用法。
createBrowserRouter + RouterProvider
creactBrowserRouter + <RouterProvider/> 取代了原本的 <BrowserRouter/> 元件,讓路由的結構能更好閱讀及管理。creactBrowserRouter 可以接受以物件形式組成的 Nest Router Config,而在引入根元件時,只要使用 <RouterProvider /> 再把 creactBrowserRouter API 產生的 router 傳入就可以了。
物件形式:
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
    children: [
      {
        path: "about",
        element: <About />,
      },
    ],
  },
]);
export default router;
除了物件形式,也可以接受使用像原本的 Nest Component 的結構,需要另外使用 createRoutesFromElements 來包裹:
import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
} from "react-router-dom";
const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path='/' element={<Home />}>
      <Route path='about' element={<About />} />
    </Route>
  )
);
export default router;
使用 <RouterProvider/>:
import { RouterProvider } from "react-router-dom";
import router from "./router";
function App() {
  return <RouterProvider router={router} />;
}
Data Loading
以往在進到某個頁面,需要 call API 來渲染畫面,通常會在 useEffect 中 call API,然後把資料存在 state 中,再用 state 來渲染畫面。但是這樣的寫法會有幾個問題:
- 進到頁面時,畫面會先渲染一次,然後 call API,再渲染一次
- 如果 API call 失敗,畫面會一直停留在 loading 狀態
所以在設計的時候都會需要建立幾個 flag 來控制,例如 isLoading、isError、data 等等,這樣的寫法會讓程式碼變得很冗長。如果有使用像 TanStack Query、RTK Query 等套件,可以幫助我們處理這些問題。不過如果純粹想在 Router 切換時去管理 Data Fetching,就可以使用 v6 的 loader 功能。
首先建立一個 fetchData() 函式:
export const fetchData = (url) => {
  const res = await fetch(url);
  const data = await res.json();
  return data;
};
傳入 loader 參數中:
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
    children: [
      {
        path: "about",
        element: <About />,
        loader: () => fetchData("API_URL"),
      },
    ],
  },
]);
這樣就可以在進到 <About/> 頁面時,自動 call API 等待資料回傳後才會渲染畫面。react-router-dom 也提供了 useLoaderData 的 hook,可以在頁面中取得回傳的 data:
import { useLoaderData } from "react-router-dom";
const About = () => {
  const data = useLoaderData();
  return <div>{JSON.stringify(data)}</div>;
};
export default About;
管理 loading 狀態
不過不像其他狀態管理套件會回傳 isLoading 或 isError 等狀態,useLoaderData 只會回傳 data。所以如果要顯示 loading 狀態,有兩種方法:
- 
使用 useNavigationhookuseNavigation會回傳當前的 state 狀態,總共會有四種狀態:- idle:頁面渲染沒有被擱置時。
- submitting:route action 被呼叫時,會用在表單送出。
- loading:route loader 被呼叫時。
 以 loader 來說會經歷以下階段: idle → loading → idle而 action 就會多一個 submitting 階段: idle → submitting → loading → idle
Route Action 可以參考 action,主要是來處理表單送出、頁面跳轉等行為。除了 "GET" 以外的方法,都會被視為 "submitting" 狀態。
我們就可以在 layout 元件用這個 hook 來判斷是否在 loading 狀態:
import { useNavigation } from "react-router-dom";
const Layout = () => {
  const { state } = useNavigation();
  if (state === "loading") return <div>Loading...</div>;
  if (state === "idle") return <Outlet />;
};
export default Layout;
要注意的是,如果使用 useNavigation 來判斷是否在 loading 狀態,需要寫在 layout 元件中,也就是含有 <Outlet/> 的元件中,如果寫在子元件中,會一直是 "idle" 狀態。
- 
使用 <Suspense/>+defer+<Await/><Suspense/>是 React 18 提供的功能,直翻的話是「懸念」,在開發上使用時可以翻譯成未完成狀態。我的理解是進行非同步處理時,等待事情完成才進行渲染。常會搭配lazy來進行元件的延遲載入。React router v6 提供 deferAPI 來讓 loader 裡的函式進行延遲執行,並且搭配<Await/>來接收 loader 回傳的資料。使用 defer延遲執行:router/index.jsimport { createBrowserRouter } from "react-router-dom";
 const router = createBrowserRouter([
 {
 path: "/",
 element: <Home />,
 children: [
 {
 path: "about",
 element: <About />,
 loader: () =>
 defer({
 data: fetchData("API_URL"),
 }),
 },
 ],
 },
 ]);使用 <Suspense/>+<Await/>:pages/About.jsximport { Suspense } from "react";
 import { useLoaderData } from "react-router-dom";
 const About = () => {
 const { data } = useLoaderData();
 return (
 <Suspense fallback={<div>Loading...</div>}>
 <Await resolve={data}>
 {(resData) => <div>{JSON.stringify(resData)}</div>}
 </Await>
 </Suspense>
 );
 };
 export default About;<Suspense/>的 fallback 傳入的就是當<Await/>還沒 resolve data 時顯示的 loading 畫面,而<Await/>的 Children 會傳入 resolve 後的 response data,可以直接在 Children 中使用。
處理 Error 顯示
如果 API call 失敗,可以在 router 中傳入 errorElement 來 render 錯誤的畫面處理:
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
    children: [
      {
        path: "about",
        element: <About />,
        loader: () => fetchData("API_URL"),
        errorElement: <div>Something went wrong</div>,
      },
    ],
  },
]);
如果發生錯誤但是沒有設定 errorElement,就會冒泡到上層的 router 去尋找,所以也可以在根元件中設定 errorElement 來統一處理。
處理重新導向
在一些有權限限制的頁面,也可以在 loader 的時候來處理導轉,就可以不用進到當前頁面再進行判斷:
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
    children: [
      {
        path: "about",
        element: <About />,
        loader: () => {
          const user = fetchData("USER_URL");
          if (user.role !== "admin") {
            return redirect("/");
            // throw redirect("/")
          }
          return user;
        },
        errorElement: <div>Something went wrong</div>,
      },
    ],
  },
]);
這邊可以使用 return 或是 throw 的方式來結束 loader。
NavLink Active 樣式
在 v6 之前,<NavLink/> 有提供 activeClassName、activeStyle 來設定當前頁面的 Nav link 樣式,但是在 v6 中,這兩個屬性已經被移除了。取而代之的是可以在 className 、 style 中傳入一個函式,這個函式會接收兩個參數,isActive 及 isPending,可以讓我們操作 NavLink Active 更彈性。
import { NavLink } from "react-router-dom";
const Layout = () => {
  return (
    <div>
      <NavLink
        style={({ isActive, isPending }) => ({
          backgroundColor: isActive ? "blue" : "white",
          color: isPending ? "gray" : "black",
        })}
        to='/'
      >
        Home
      </NavLink>
    </div>
  );
};
isActive 會判斷 to 所指定的頁面,當在該頁面時就會回傳 true;isPending 則是用在頁面切換,使用 loader 時會比較明顯的看到效果。
這邊我刻意延長 loader 的時間,設定 isPending 則顏色為紅色:
