在建置網站時,通常不會只有一個頁面包含所有的功能,我們通常會在網站上設置導覽列,讓使用者點擊連結後看到不同的功能頁面,需要藉由路由
機制來得到不同頁面。
傳統從瀏覽器輸入網址,讓伺服器上的後端程式碼回應不同 Url 對應的網頁檔案,稱為後端路由。缺點是每次換網址,就要重讀檔案,讓頁面重繪。但大多數的時候,頁面上的 Layout 固定,只有主要區塊的內容會依分頁而不同,整個頁面重新渲染很浪費效能。
為了解決上面的問題,前端工程提出了解決方案 - 當換頁的時候,只用 JS 改變頁面裡不一樣的地方。這樣換頁時就不需要整頁刷新,很像在使用一個應用程式一樣,所以這樣的網頁程式又稱為 Single Page Application (SPA)。
React Router DOM
套件,就是 React 達成前端路由的方法之一。
React Router DOM 提供了相當完整的前端路由功能,因為篇幅有限的關係,在本篇文章會藉由官方提供的 Tutorial 邊做邊學習,而沒有介紹到的功能,列為日後鐵人賽的補充。
在 2021 年 11 月初,React Router 發表了 v6 的正式版本,所以本篇文章也著重於介紹 v6 版本的 API。
React Router DOM 沒有預設在 create-react-app
的相依套件中,所以必須要自行手動加裝。
npm install react-router-dom
為了 Demo 方便,這裡使用 CodeSandBox 來示範。
如果是使用 CodeSandBox React 模版可以依圖配置初始專案模版
以下 CodeSandBox 連結,已先幫大家建置好加上套件相依性及 Tutorial 所需檔案,請大家 Fork 做修改。
https://codesandbox.io/s/react-router-tutorial-setup-n2wbov
React Router DOM 提供好幾種前端的路由機制/路由器 (Router),使用者可以依照應用程式運行的環境去選擇。
在 v6.4 Data API 提供以下建置 Router 的函式
這個 Router 是使用 HTML5 原生的 DOM History API 更新 URL 及瀏覧器歷史記錄。一般都是建議使用 BrowserRouter。
// src/main.jsx
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/", // 首頁
element: 要加入的首頁元件
}, {
path: "/...", // 功能頁面
element: 要加入的功能頁面元件
}
]);
如果你的網站運行環境不支援使用 BrowserRouter,則可以使用 HashRouter。他會在 URL 加上 hash 做為識別,除此之外它的功能會與 BrowserRouter 相同。
不使用 Browser History 記錄,通常是用於測試與非瀏覽器的環境。
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
+ <RouterProvider router={router} />
</React.StrictMode>
);
export default function Root() {
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div id="search-spinner" aria-hidden hidden={true} />
<div className="sr-only" aria-live="polite"></div>
</form>
<form method="post">
<button type="submit">New</button>
</form>
</div>
<nav>
<ul>
<li>
<a href={`contacts/1`}>Your Name</a>
</li>
<li>
<a href={`contacts/2`}>Your Friend</a>
</li>
</ul>
</nav>
</div>
<div id="detail"></div>
</>
);
}
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import "./index.css";
// imort Root 元件
import Root from "./routes/root";
// 建立 Router (用 createBrowserRouter 建立 BrowserRouter)
const router = createBrowserRouter([
{
path: "/",
element: <Root /> // 要加入的首頁元件
}
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
{/* 使用 RouterProvider 把 Router 加入專案中 */}
<RouterProvider router={router} />
</React.StrictMode>
);
目前操作結果:https://codesandbox.io/s/react-router-tutorial-add-a-router-yd8ste
<RouterPrivider>
:https://reactrouter.com/en/main/routers/router-provider如果點選 Sidebar 上的一個連結,會顯示錯誤如下圖,這是 React 內建的錯誤,應該要設計錯誤頁面的元件,更友善的提示錯誤給使用者。
import { useRouteError } from "react-router-dom";
const 錯誤頁面的元件 = () => {
// 使用 useRouteError 取得路由錯誤資訊
const error = useRouteError();
return (
// 把 error 資料顯示在你的 jsx 上
...
{error.statusText}
{error.message}
);
};
export default 錯誤頁面的元件;
在 errorElement
加上錯誤頁面的元件
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
+ errorElement: 錯誤頁面的元件,
},
]);
// src/error-page.jsx
import { useRouteError } from "react-router-dom";
const ErrorPage = () => {
// 使用 useRouteError 取得路由錯誤資訊
const error = useRouteError();
console.error(error);
return (
<div id="error-page">
{/* 把 error 資料顯示在你的 jsx 上 */}
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
};
export default ErrorPage;
+ import ErrorPage from "./error-page";
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // 要加入的首頁元件
+ errorElement: <ErrorPage /> // 要加入的錯誤頁面的元件
}
]);
目前操作結果:https://codesandbox.io/s/react-router-tutorial-handling-notfound-erros-ykm7kq
route - errorElement
: https://reactrouter.com/en/main/route/error-element
Sidebar 的第一個連結是 contacts/1,所以現在我們要處理的是要建立 Contact 頁面元件,且把 Contact 加入 Router 中
新增 routes/contact.jsx,請複製貼上此 gist 程式碼。
+ import Contact from "./routes/contact";
...
const router = createBrowserRouter([
{...},
+ {
+ path: "contacts/:contactId",
+ element: <Contact />
+ },
]);
目前操作結果:https://codesandbox.io/s/react-router-tutorial-add-contact-route-tohm2t
用以維持目前路由當下元件的 Layout 不變,只更換需要變成的元件畫面。
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: 錯誤頁面的元件,
+ children: [
+ {
+ path: 巢狀路由路徑
+ element: 對應的元件
+ },
+ ],
},
]);
<Outlet />
用於加在當父層元件,要顯示其對應巢狀路由下的子元件。
const 父元件() {
return (
<>
{/* 父元件上不會路著巢狀路由變動的 DOM Element */}
<div>
{/* 找到適合的位置插入 <Outlet /> */}
<Outlet />
</div>
</>
);
}
上一步的呈現結果,沒有套用到 Root 元件下的 Layout,所以不會顯示 Sidebar。接下來就來設置巢狀路由讓 Contact 元件在 Root 元件下被顯示出來。
// src/main.jsx
...
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
...
// src/routes/root.jsx
import { Outlet } from "react-router-dom";
export default function Root() {
return (
<>
...
<div id="detail">
<Outlet />
</div>
</>
);
}
目前操作結果:https://codesandbox.io/s/react-router-tutorial-nested-route-wb2k70
route
: https://reactrouter.com/en/main/route/route
Components - <Outlet>
: https://reactrouter.com/en/main/components/outlet
在 Root 元件裡,因為還是用傳統的 <a href="xxx" />
的連結,這樣的設定還是會去跟伺服器做整個頁面的資源要求,所以會發現畫面還是閃了一下。
要套用前端路由控制,就要把a連結
改為 React Router DOM 提供的 <Link>
元件。
import { ..., Link } from "react-router-dom";
...
<Link to={`xxx/xx`}>Your Link</Link>
a連結
改為<Link>
元件。// src/routes/root.jsx
- import { Outlet } from "react-router-dom";
+ import { Outlet, Link } from "react-router-dom";
...
- <a href={`contacts/1`}>Your Name</a>
...
- <a href={`contacts/2`}>Your Friend</a>
...
+ <Link to={`contacts/1`}>Your Name</Link>
...
+ <Link to={`contacts/2`}>Your Friend</Link>
...
目前操作結果:https://codesandbox.io/s/react-router-tutorial-csr
+ const featureLoader = () => {...};
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
+ loader: featureLoader, // 載入資料的 loader function
children: [...],
},
]);
在元件中使用 useLoaderData
來取得預先載入資料
import { ..., useLoaderData } from "react-router-dom";
const 元件 = () => {
const { ... } = useLoaderData();
};
loader
// src/main.jsx
import { getContacts } from "../contacts";
const rootLoader = async () => {
const contacts = await getContacts();
return { contacts };
};
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // 要加入的首頁元件
errorElement: <ErrorPage />, // 要加入的錯誤頁面的元件
loader: rootLoader, // 加上載入資料的 loader function
children: [...],
},
]);
import {..., useLoaderData} from "react-router-dom";
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
{/* other code */}
<nav>
{
contacts.length
? contacts.length
: (<p><i>No contacts</i></p>)
}
</nav>
{/* other code */}
</div>
</>
);
}
因為還沒有 Contacts 資料,所以顯示 No Contacts
目前操作結果:https://codesandbox.io/s/react-router-tutorial-loader-u65vkq
route - loader
: https://reactrouter.com/en/main/route/loader
Hooks - useLoaderData
:https://reactrouter.com/en/main/hooks/use-loader-data在頁面元件上設定 React Router Form 元件,然後在 React Router 的 Route 設定加上 action 對應,此時在該頁面元件上做 Form Submit 時,就會執行剛剛設定的 action 對應。
import { ..., Form } from "react-router-dom";
...
<Form method="post">
<button type="submit">...</button>
</Form>
featureAction 通常會執行要送出給後端伺服器的資料
+ const featureAction = async () => {...};
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: featureLoader,
+ action: featureAction, // // 加上頁面元件 <Form /> 的 action 對應
children: [...],
},
]);
// src/routes/root.jsx
import { ..., Form } from "react-router-dom";
...
<Form method="post">
<button type="submit">New</button>
</Form>
// src/main.jsx
import { ..., createContact } from "../contacts";
...
const rootAction = async () => {
await createContact();
};
...
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // 要加入的首頁元件
errorElement: <ErrorPage />, // 要加入的錯誤頁面的元件
loader: rootLoader, // 加上載入資料的 loader function
action: rootAction, // 加上 <Root/> 元件 <Form/> 的 action 對應
// 設定的巢狀路由
children: [...],
},
]);
畫面上按下 New 會執行 createContact(),建立 Contact 假資料
目前操作結果:https://codesandbox.io/s/react-router-tutorial-form-and-action-9m4gyo
route - action
: https://reactrouter.com/en/main/route/action
Components - <Form>
:https://reactrouter.com/en/main/hooks/use-loader-data前面學習到可以利用 Router Loader 在路由切換元件前,預先載入資料,如果我們的路由是有帶入資料的Key值時,那麼我們在切換到顯示該資料詳細內容的頁面元件時,也可以在該路由上設定帶參數 loader function。
當在 Route 上使用 :
符號,表示 : 後面的變數,就是動態的參數,稱之為 URL Params。
{
path: 'xxx/:id',
element: 元件,
loader: async (params) => {
...
const {id} = params;
...
return getXXXById(id);
},
}
前面學習到 Form 元件的 Submit,會對應到該頁面元件路由設定的 Action,所以在 Aciton Function,我們可以使用 request.formData()
,取得 Form 元件上設定的欄位資料。
{
path: 'xxx/:id',
element: 元件,
loader: async (params) => {...},
action: async ({ request, params }) => {
const formData = await request.formData();
}
}
我們在 loader 或 action 的 funciton 中,如果操作資料時,判斷有需要導向至別的頁面元件時,就可以使用 redirect
重定向到另一個路由。
redirect("/new-loacation-url");
接下來就加上顯示聯絡人詳細資料,及編輯聯絡人資料的表單功能,最後調整新增聯絡人功能也使用表單功能,來說明上述的概念。
// src/main.jsx
import {
...,
getContact,
} from "./contacts";
const contactLoader = async ({ params }) => {
console.log('params.contactId', params.contactId);
return getContact(params.contactId);
};
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // 要加入的首頁元件
...
// 設定的巢狀路由
children: [
{
path: "contacts/:contactId",
element: <Contact />,
// 加上顯示聯絡人詳細資料的 loader funciton
loader: contactLoader
}
]
}
]);
// src/routes/contact.jsx
import { useLoaderData, Form } from "react-router-dom";
// 改成用 useLoaderData 拿到聯絡人資料
const contact = useLoaderData();
點擊 Sidebar 上的 No Name 項目,路由上的 :contactId,會做為 params.contactId
,帶入到 loader function 中,程式用contactId 抓取完聯絡人詳細資料,就會將資料顯示在頁面上。
編輯聯絡人資料的頁面元件的程式碼,請至 gist 抓取。節錄重點程式碼如下:
// src/routes/edit.jsx
import { Form, useLoaderData } from "react-router-dom";
...
const contact = useLoaderData();
...
<Form method="post" id="contact-form">
...
<p>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</p>
</Form>
...
redirect
轉頁至顯示聯絡人詳細資料頁面。// src/main.jsx
import { ..., updateContact } from "./contacts";
...
const contactEditAction = async ({ request, params }) => {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await updateContact(params.contactId, updates);
return redirect(`/contacts/${params.contactId}`);
};
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // 要加入的首頁元件
...
// 設定的巢狀路由
children: [
{...},
{
path: "contacts/:contactId/edit",
element: <EditContact />,
loader: contactLoader,
action: contactEditAction
}
]
}
]);
// src/main.jsx
// 修改 rootAciton 如下
const rootAction = async () => {
const contact = await createContact();
return redirect(`/contacts/${contact.id}/edit`);
};
目前操作結果:https://codesandbox.io/s/react-router-tutorial-loader-and-action-3qr3p8
route - loader
: https://reactrouter.com/en/main/route/loader
route - action
: https://reactrouter.com/en/main/route/action
Fetch Utilities - redirect
:https://reactrouter.com/en/main/fetch/redirectReact Router DOM 官網的 Tutorial 教學,到此為止,我們完成了設定路由對應各種操作頁面的基本功能,接下來會再更深入的介紹 React Router DOM 的進階操作。
https://reactrouter.com/en/6.4.1/start/tutorial
https://ithelp.ithome.com.tw/articles/10188245
https://ithelp.ithome.com.tw/articles/10226056
https://ithelp.ithome.com.tw/articles/10226370
https://ithelp.ithome.com.tw/articles/10282773
https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/implementing-react-router-dom-bf986888f2ce