【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 狀態,有兩種方法:
-
使用
useNavigation
hookuseNavigation
會回傳當前的 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 提供
defer
API 來讓 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
則顏色為紅色: