iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0

說明

在建置網站時,通常不會只有一個頁面包含所有的功能,我們通常會在網站上設置導覽列,讓使用者點擊連結後看到不同的功能頁面,需要藉由路由機制來得到不同頁面。

後端路由

傳統從瀏覽器輸入網址,讓伺服器上的後端程式碼回應不同 Url 對應的網頁檔案,稱為後端路由。缺點是每次換網址,就要重讀檔案,讓頁面重繪。但大多數的時候,頁面上的 Layout 固定,只有主要區塊的內容會依分頁而不同,整個頁面重新渲染很浪費效能。

前端路由

為了解決上面的問題,前端工程提出了解決方案 - 當換頁的時候,只用 JS 改變頁面裡不一樣的地方。這樣換頁時就不需要整頁刷新,很像在使用一個應用程式一樣,所以這樣的網頁程式又稱為 Single Page Application (SPA)。

React Router DOM 套件,就是 React 達成前端路由的方法之一。

React Router DOM

React Router DOM 提供了相當完整的前端路由功能,因為篇幅有限的關係,在本篇文章會藉由官方提供的 Tutorial 邊做邊學習,而沒有介紹到的功能,列為日後鐵人賽的補充。

在 2021 年 11 月初,React Router 發表了 v6 的正式版本,所以本篇文章也著重於介紹 v6 版本的 API。

Setup - 安裝及前置作業

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

Add a Router 新增 Router 用以設定每個頁面元件的路由配置

選擇路由機制 Picking a Router

React Router DOM 提供好幾種前端的路由機制/路由器 (Router),使用者可以依照應用程式運行的環境去選擇。

在 v6.4 Data API 提供以下建置 Router 的函式

  • createBrowserRouter

這個 Router 是使用 HTML5 原生的 DOM History API 更新 URL 及瀏覧器歷史記錄。一般都是建議使用 BrowserRouter。

// src/main.jsx
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
  {
    path: "/", // 首頁
    element: 要加入的首頁元件
  }, {
    path: "/...", // 功能頁面
    element: 要加入的功能頁面元件
  }
]);
  • createHashRouter

如果你的網站運行環境不支援使用 BrowserRouter,則可以使用 HashRouter。他會在 URL 加上 hash 做為識別,除此之外它的功能會與 BrowserRouter 相同。

  • createMemoryRouter

不使用 Browser History 記錄,通常是用於測試與非瀏覽器的環境。

使用 RouterProvider 把 Router 加入專案中

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
+    <RouterProvider router={router} />
  </React.StrictMode>
);

範例操作

  • 增加一個首頁元件 routes/root.jsx
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>
    </>
  );
}
  • 修改 main.jsx 加上 Router 及 RouterProvider
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

API 參考

  • Picking a Router:https://reactrouter.com/en/main/routers/picking-a-router
  • <RouterPrivider>:https://reactrouter.com/en/main/routers/router-provider

Handling Not Found Errors 控制找不到路由時的錯誤

如果點選 Sidebar 上的一個連結,會顯示錯誤如下圖,這是 React 內建的錯誤,應該要設計錯誤頁面的元件,更友善的提示錯誤給使用者。

製作錯誤頁面的元件

import { useRouteError } from "react-router-dom";

const 錯誤頁面的元件 = () => {
  // 使用 useRouteError 取得路由錯誤資訊
  const error = useRouteError();
  return (
    // 把 error 資料顯示在你的 jsx 上
    ...
    {error.statusText}
    {error.message}
  );
};
export default 錯誤頁面的元件;

在 Router 加上錯誤頁面的元件

errorElement 加上錯誤頁面的元件

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
+   errorElement: 錯誤頁面的元件,
  },
]);

範例操作

  • 新增錯誤頁面元件 src/error-page.jsx
// 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;
  • 在 Router 加上錯誤頁面的元件 (調整 src/main.jsx)
+ import ErrorPage from "./error-page";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />, // 要加入的首頁元件
+   errorElement: <ErrorPage /> // 要加入的錯誤頁面的元件
  }
]);
  • 以上步驟完成後,會看到以下的畫面

目前操作結果:https://codesandbox.io/s/react-router-tutorial-handling-notfound-erros-ykm7kq

API 參考

Add Contact Route - 範例操作

Sidebar 的第一個連結是 contacts/1,所以現在我們要處理的是要建立 Contact 頁面元件,且把 Contact 加入 Router 中

  • 建立 Contact 頁面元件

新增 routes/contact.jsx,請複製貼上此 gist 程式碼。

  • 把 Contact 頁面元件加入 Router 中
+ import Contact from "./routes/contact";
...
const router = createBrowserRouter([
  {...},
+ {
+   path: "contacts/:contactId",
+   element: <Contact />
+ },
]);
  • 以上步驟完成後,會看到以下的畫面

目前操作結果:https://codesandbox.io/s/react-router-tutorial-add-contact-route-tohm2t

API 參考

Nested Routes 設置巢狀路由/嵌套路由

用以維持目前路由當下元件的 Layout 不變,只更換需要變成的元件畫面。

在 Router 下設置巢狀路由

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: 錯誤頁面的元件,
+   children: [
+     {
+       path: 巢狀路由路徑
+       element: 對應的元件
+     },
+   ],    
  },
]);

加上 <Outlet />

用於加在當父層元件,要顯示其對應巢狀路由下的子元件。

const 父元件() {
  return (
    <>
      {/* 父元件上不會路著巢狀路由變動的 DOM Element */}
      <div>
        {/* 找到適合的位置插入 <Outlet /> */}
        <Outlet />
      </div>
    </>
  );
}

範例操作

上一步的呈現結果,沒有套用到 Root 元件下的 Layout,所以不會顯示 Sidebar。接下來就來設置巢狀路由讓 Contact 元件在 Root 元件下被顯示出來。

  • 在 Router 下設置巢狀路由
// src/main.jsx
...
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);
...
  • 在 Root 元件下,加入
// 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

API 參考

Client-Side Routing 前端路由控制 (CSR)

在 Root 元件裡,因為還是用傳統的 <a href="xxx" /> 的連結,這樣的設定還是會去跟伺服器做整個頁面的資源要求,所以會發現畫面還是閃了一下。

要套用前端路由控制,就要把a連結 改為 React Router DOM 提供的 <Link> 元件。

import { ..., Link } from "react-router-dom";
...
<Link to={`xxx/xx`}>Your Link</Link>

範例操作

  • 在 Root 元件,把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>
...
  • 以上步驟完成後,會看到以下的畫面 (點擊連結直接顯示UI,不會閃一下)

目前操作結果:https://codesandbox.io/s/react-router-tutorial-csr

API 參考

利用 Router Loader 在路由切換元件前,預先載入資料

Router Loader

+ const featureLoader = () => {...};
  const router = createBrowserRouter([
    {
      path: "/",
      element: <Root />,
      errorElement: <ErrorPage />,
+     loader: featureLoader, // 載入資料的 loader function
      children: [...],
    },
  ]);

useLoaderData

在元件中使用 useLoaderData 來取得預先載入資料

import { ..., useLoaderData } from "react-router-dom";
const 元件 = () => {
  const { ... } = useLoaderData();
};

範例操作

  • 新增載入資料的function,然後在 Router 路由配置中設定給 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: [...],
  },
]);
  • 在 Root 元件使用 useLoaderData 取得預先載入資料
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

API 參考

React Router Form & Action

在頁面元件上設定 React Router Form 元件,然後在 React Router 的 Route 設定加上 action 對應,此時在該頁面元件上做 Form Submit 時,就會執行剛剛設定的 action 對應。

React Router Form 元件

import { ..., Form } from "react-router-dom";
...
<Form method="post">
  <button type="submit">...</button>
</Form>

React Router Route Action

featureAction 通常會執行要送出給後端伺服器的資料

+ const featureAction = async () => {...};           
  const router = createBrowserRouter([
    {
      path: "/",
      element: <Root />,
      errorElement: <ErrorPage />,
      loader: featureLoader,
+     action: featureAction, // // 加上頁面元件 <Form /> 的 action 對應
      children: [...],
    },
  ]);                                   

範例操作

  • 在 Root 元件加上 React Router Form 元件
// src/routes/root.jsx
import { ..., Form } from "react-router-dom";
...
<Form method="post">
  <button type="submit">New</button>
</Form>
  • 在 Router 路由配置上,加上 Action 對應
// 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

API 參考

再探 Loaders 及 Actions

URL Params in Loader

前面學習到可以利用 Router Loader 在路由切換元件前,預先載入資料,如果我們的路由是有帶入資料的Key值時,那麼我們在切換到顯示該資料詳細內容的頁面元件時,也可以在該路由上設定帶參數 loader function。

當在 Route 上使用 : 符號,表示 : 後面的變數,就是動態的參數,稱之為 URL Params。

{
  path: 'xxx/:id',
  element: 元件,
  loader: async (params) => {
    ...
    const {id} = params;
    ...
    return getXXXById(id);
  },
}

FormData in Action

前面學習到 Form 元件的 Submit,會對應到該頁面元件路由設定的 Action,所以在 Aciton Function,我們可以使用 request.formData(),取得 Form 元件上設定的欄位資料。

{
  path: 'xxx/:id',
  element: 元件,
  loader: async (params) => {...},
  action: async ({ request, params }) => {
    const formData = await request.formData();
  }
}

redirect

我們在 loader 或 action 的 funciton 中,如果操作資料時,判斷有需要導向至別的頁面元件時,就可以使用 redirect 重定向到另一個路由。

redirect("/new-loacation-url");

範例操作

接下來就加上顯示聯絡人詳細資料,及編輯聯絡人資料的表單功能,最後調整新增聯絡人功能也使用表單功能,來說明上述的概念。

  • 顯示聯絡人詳細資料 - (URL Params in Loader)
// 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 抓取完聯絡人詳細資料,就會將資料顯示在頁面上。

  • 增加編輯聯絡人資料的頁面元件 (/routes/edit.jsx)

編輯聯絡人資料的頁面元件的程式碼,請至 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>
...
  • 加上編輯頁面的路由對應(Edit Form 的 Submit 會對應 contactEditAction),當編輯更新完成,使用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
      }      
    ]
  }
]);
  • 以上步驟完成後,會看到以下的畫面

  • 調整 Root 元件上,把「New」的功能從原本的自動假資料新增,改為使用 Edit 頁面元件做新增。
// 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

API 參考

Next

React Router DOM 官網的 Tutorial 教學,到此為止,我們完成了設定路由對應各種操作頁面的基本功能,接下來會再更深入的介紹 React Router DOM 的進階操作。

Reference

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


上一篇
Day 27 React 的 CSS 解決方案
下一篇
Day 29 React Router v6 (下)
系列文
開始搞懂React生態系30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言