[react] React Router DOM v6.4 詳細介紹
資料夾與檔案結構 (建議先將檔案建好)
src
|   App.js
|   ...
|
└─── components
│   │   MainNavigation.jsx
|   |   ProductItems.jsx
|   |   ProductList.jsx
|   |   ProductForm.jsx
|   |   ProductDeferTest.jsx
│
└─── pages
│   │   Error.jsx
│   │   Home.jsx
│   │   ProductAction.jsx
│   │   ProductRoot.jsx
│   │   ProductDetail.jsx
│   │   Products.jsx
│   │   Root.jsx
說明
React Router DOM v6.4 版本新增了許多實用的功能,但如果要使用這些功能就不能用 v6 版本的 BrowserRouter,而是得使用新的 createBrowserRouter 和 RouterProvider,v6.4 版主要是著重在 data loading 和 date fetch 的部份。
之前在 這部影片 有稍微介紹過,但介紹的功能沒有那麼多,這次就一次補完。
Basic
createBrowserRouter & RouterProvider
先將 createBrowserRouter 和 RouterProvider 引入,接著使用 createBrowserRouter 建立 Router 後,在 RouterProvider 將 router 傳遞進去。
import { createBrowserRouter, RouterProvider } from "react-router-dom";import HomePage from "./pages/Home";import ProductsPage from "./pages/Products";const router = createBrowserRouter([  { path: "/", element: <HomePage /> },  {    path: "/products",    element: <ProductsPage />,  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;const HomePage = () => {  return <div>Home</div>;};export default HomePage;const ProductsPage = () => {  return <div>Products</div>;};export default ProductsPage;將 path 指定為 '/' 的意思就是我們的 Domain, http://localhost:5173,如果網頁的網址為 http://example.com ,則 '/' 就是 http://example.com 。
所以現在只要在網址輸入 http://localhost:5173(因環境而異) 就會渲染出 HomePage, http://localhost:5173/products,則渲染出 ProductsPage。
createBrowserRouter & createRoutesFromElements & RouterProvider
如果不習慣用物件的方式定義 Router,也可以使用原本的 JSX 方式,只要引入 createRoutesFromElements 和 Route 即可。
import {  createBrowserRouter,  createRoutesFromElements,  Route,  RouterProvider,} from "react-router-dom";import HomePage from "./pages/Home";import ProductsPage from "./pages/Products";const routeDefinitions = createRoutesFromElements(  <Route>    <Route path="/" element={<HomePage />} />    <Route path="/products" element={<ProductsPage />} />  </Route>);const router = createBrowserRouter(routeDefinitions);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;a tag & Link
在剛學 React Router 的時候,可能有些人會不懂為什麼要使用 Router 提供的 Link Component 來連結到我們其他的 Component,這是因為如果使用超連結 a 標籤的方式來指定路徑,當我們點擊 a 標籤時,瀏覽器會以為我們要到新的頁面,進而重新發送 Request 取得我們要的頁面,等同於重新整理 React Application 的意思,而重新整理頁面的話,我們的 React State 就會全部遺失,所以這顯然不是個好方法。
而 React Router 提供的 Link 能防止瀏覽器送出 Request(Prevent Default Behavior),相反的,Link Component 檢查我們的 URL Path,如果有在 Router 中定義該 Path 的話,就直接渲染 Path 中定義的 element(component) 畫面。
import { Link } from "react-router-dom";const HomePage = () => {  return (    <div>      <h1>Home</h1>      <p>        Go to <Link to="/products">products</Link>      </p>    </div>  );};export default HomePage;Layout & Outlet
現在如果要新增一個 Navbar 來讓使用者能夠到達其他頁面的話,我們可以在 Router 定義一個新的 Route。
先新增一個 Root Layout 和 Navbar:
const RootLayout = () => {  return (    <div>      <h1>Root Layout</h1>    </div>  );};export default RootLayout;import { Link } from "react-router-dom";const MainNavigation = () => {  return (    <header>      <ul>        <li>          <Link to="/">Home</Link>        </li>        <li>          <Link to="/products">Products</Link>        </li>      </ul>    </header>  );};export default MainNavigation;因為要讓 Navbar 顯示在每一個頁面的上方,所以要重新定義一下 Router,先回到 App.jsx,將 Router 改為以下:
import { createBrowserRouter, RouterProvider } from "react-router-dom";import "./App.css";import HomePage from "./pages/Home";import ProductsPage from "./pages/Products";import RootLayout from "./pages/Root";const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    children: [      { path: "", element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,      },    ],  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;可以看到我們多了一個 children 的屬性,並將先前定義的 Route 搬到 children 裡面,但這時候回到畫面,只會看到 RootLayout 的畫面,原先 HomePage 的畫面消失了。
這是因為我們必須在 Parent Route(RootLayout) 中,引入 Outlet Component 並渲染,Outlet 的作用為讓 Parent Route 能夠渲染出 Child Route 的畫面。
import { Outlet } from "react-router-dom";import MainNavigation from "../components/MainNavigation";const RootLayout = () => {  return (    <div>      <MainNavigation />      <Outlet />    </div>  );};export default RootLayout;Error Element
現在嘗試在網址的地方打上不存在的 Path,會看到以下 404 錯誤畫面:

如果要客製化 404 Not Found 頁面或是錯誤頁面,則可以在 Router 定義 errorElement 屬性,並把要顯示的 Component 定義好。
import MainNavigation from "../components/MainNavigation";const ErrorPage = () => {  return (    <>      <MainNavigation />      <div>        <h1>An error occurred!!</h1>        <p>Could not find this page</p>      </div>    </>  );};export default ErrorPage;import { createBrowserRouter, RouterProvider } from "react-router-dom";import "./App.css";import ErrorPage from "./pages/Error";import HomePage from "./pages/Home";import ProductsPage from "./pages/Products";import RootLayout from "./pages/Root";const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    children: [      { path: "", element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,      },    ],  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;NavLink
如果想要讓使用者了解目前在哪個頁面,則可以使用 NavLink,NavLink 提供了 isActive 的屬性,當 isActive 為 true 時,代表使用者目前在該頁面,所以可以簡單做個判斷,並附上簡單的 CSS。
import { NavLink } from "react-router-dom";import classes from "./MainNavigation.module.css";const MainNavigation = () => {  return (    <header className={classes.header}>      <ul className={classes.list}>        <li>          <NavLink            to="/"            className={({ isActive }) =>              isActive ? classes.active : undefined            }            // style={({ isActive }) => ({            //   textDecoration: isActive ? "underline" : "none",            // })} 這樣寫 inline-style 也可以 看個人          >            Home          </NavLink>        </li>        <li>          <NavLink            to="/products"            className={({ isActive }) =>              isActive ? classes.active : undefined            }            // style={({ isActive }) => ({            //   textDecoration: isActive ? "underline" : "none",            // })} 這樣寫 inline-style 也可以 看個人          >            Products          </NavLink>        </li>      </ul>    </header>  );};export default MainNavigation;useNavigate
有時候我們會希望觸發某個 function 後,導向到其他頁面,這時候就可以使用 useNavigate 來達到該功能。
這邊只是 Demo 用,後續不會將該程式碼新增到後面的教學。
import { Link, useNavigate } from "react-router-dom";const HomePage = () => {  const navigate = useNavigate();  function navigateHandler() {    navigate("/products"); // 當 navigateHandler function 觸發,導向到 /products  }  return (    <div>      <h1>Home</h1>      <p>        Go to <Link to="/products">products</Link>      </p>    </div>  );};export default HomePage;Dynamic Routes
如果要達到動態 Route 的功能,只需在 path 後面加上 :id 即可。
import { createBrowserRouter, RouterProvider } from "react-router-dom";import "./App.css";import ErrorPage from "./pages/Error";import HomePage from "./pages/Home";import ProductDetailPage from "./pages/ProductDetail";import ProductsPage from "./pages/Products";import RootLayout from "./pages/Root";const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    children: [      { path: "", element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,      },      { path: "products/:productId", element: <ProductDetailPage /> },    ],  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;在 ProductDetail.jsx 中,只需將 useParams 引入,即可取得我們在 path 定義的 id。
import { useParams } from "react-router-dom";const ProductDetailPage = () => {  const params = useParams();  return <div>ProductDetail {params.productId}</div>;};export default ProductDetailPage;然後將 Products.jsx 中的程式碼改成以下,就完成動態 Routes 的功能了:
import { Link } from "react-router-dom";const PRODUCTS = [  { id: "p1", title: "Product 1" },  { id: "p2", title: "Product 2" },  { id: "p3", title: "Product 3" },];const ProductsPage = () => {  return (    <div>      <h1>Products Page</h1>      <ul>        {PRODUCTS.map((product) => (          <li key={product.id}>            <Link to={`/products/${product.id}`}>{product.title}</Link>          </li>        ))}      </ul>    </div>  );};export default ProductsPage;Relative Path & Relative Route
假設我們想要在 ProductDetail.jsx 中,實作回到上一頁功能,也就是回到 Product.jsx,你可能會這樣做:
import { Link, useParams } from "react-router-dom";const ProductDetailPage = () => {  const params = useParams();  return (    <div>      <h1>ProductDetail {params.productId}</h1>      <p>        <Link to="..">Back</Link>      </p>    </div>  );};export default ProductDetailPage;.. 是回到上一層的意思,而 . 是當前目錄。
但當我們點擊 Back 後,會發現不是回到 Product.jsx,反而是回到 Home.jsx。
這就要從我們設定的 Router 開始解釋,先看一下先前設定的 Router,我們的 Parent Route 在程式碼第 3 行,也就是 path:"/",而底下的 Child Route 為程式碼第 7 行至第 12 行。
這時候如果在 path:"products/:productId" 底下回到上一層,則是會回到 Parent Route,也就是 path:"/"。
const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    children: [      { path: "", element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,      },      { path: "products/:productId", element: <ProductDetailPage /> },    ],  },]);這是因為 React Router Dom 的 Link Component 預設的 relative 屬性是 route,會根據我們設置的 Router 父子關係而有所不同。
但 relative 還有提供另一個值讓我們使用,叫做 path,將 relative 屬性更改為 path 後,就可以解決上述的問題。
path 屬性是將我們當前的網址移除一個 segment,所以假設我們的網址是 http://localhost:5173/products/p1,回到上一層 .. 就是 http://localhost:5173/products。
import { Link, useParams } from "react-router-dom";const ProductDetailPage = () => {  const params = useParams();  return (    <div>      <h1>ProductDetail {params.productId}</h1>      <p>        <Link to=".." relative="path">          Back        </Link>      </p>    </div>  );};export default ProductDetailPage;Index
再來看一下我們的 Router,不知道你有沒有發現我們的 <HomePage /> 主頁面的 path 是空值 "",空值的意思就是匹配到 Parent Route,也就是 path : "/"。
const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    children: [      { path: "", element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,      },      { path: "products/:productId", element: <ProductDetailPage /> },    ],  },]);如果不想將 path 定義為空值來匹配 Parent Route 的話,可以改為 index : true,這樣定義跟 path : "/" 是一樣的。
const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    children: [      { index: true, element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,      },      { path: "products/:productId", element: <ProductDetailPage /> },    ],  },]);Advanced
loader
如果要取得外部 API 資料的話,基本上都會使用 useEffect 搭配 Fetch Function,但 React Router Dom 也有提供 Data Fetching 的功能,叫做 loader。
要使用 loader 的話,先在 Products.jsx 定義它然後 export ,也順便將 Products.jsx 裡面的程式碼修改一下:
import ProductsList from "../components/ProductsList";const ProductsPage = () => {  return <ProductsList />;};export default ProductsPage;export const loader = async () => {  const response = await fetch("https://dummyjson.com/products?limit=5");  const data = await response.json();  return data.products;};const ProductsList = () => {  return <div>ProductsList</div>;};export default ProductsList;之後回到 App.jsx 在 path: "products" 的地方新增 loader 屬性,並將剛剛的 loader 帶進去。
import { createBrowserRouter, RouterProvider } from "react-router-dom";import "./App.css";import ErrorPage from "./pages/Error";import HomePage from "./pages/Home";import ProductDetailPage from "./pages/ProductDetail";import ProductsPage, { loader as ProductsLoader } from "./pages/Products";import RootLayout from "./pages/Root";const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    children: [      { index: true, element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,        loader: ProductsLoader,      },      { path: "products/:productId", element: <ProductDetailPage /> },    ],  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;注意 loader 是在 Client-Side 執行,並不是 Server-Side,所以你可以在 loader 裡面使用任何瀏覽器的 Function,例如:localStorage、cookie 等。
現在進入到 Products 頁面,應該會發現 Products 頁面卡了一下才顯示畫面,這時候可以開啟 Networks 來看一下,會發現進來 Products 頁面時,我們發了一筆 API Request。

React Router Dom 會等到 loader 執行完,才去渲染畫面,意思就是要等資料取完才會渲染畫面,後面會講在取資料時要怎麼顯示 Loading 文字在畫面上。
如果要取得 API 資料的話,我們可以在 Products.jsx 引入 useLoaderData。
import { useLoaderData } from "react-router-dom";import ProductsList from "../components/ProductsList";const ProductsPage = () => {  const products = useLoaderData();  console.log(products);  return <ProductsList />;};export default ProductsPage;export const loader = async () => {  const response = await fetch("https://dummyjson.com/products?limit=5");  const data = await response.json();  return data.products;};
loader scope
可以發現我們在 Products.jsx 中渲染了 ProductsList Component,而因為 ProductsList 包含在 Products 底下,所以 ProductsList 也可以使用 useLoaderData 來取得 API 資料。
import ProductsList from "../components/ProductsList";const ProductsPage = () => {  return <ProductsList />;};export default ProductsPage;export const loader = async () => {  const response = await fetch("https://dummyjson.com/products?limit=5");  const data = await response.json();  return data.products;};import { useLoaderData } from "react-router-dom";const ProductsList = () => {  const products = useLoaderData();  console.log("ProductsList : ", products);  return <div>ProductsList</div>;};export default ProductsList;
useNavigation
前面有提到,loader 執行完才會渲染畫面,所以我們可以使用 useNavigation,來判斷目前的狀態為何。
useNavigation 會提供 state,當我們在取得資料時,state 為 loading,而其他時間則為 idle,所以可以判斷當下的 state 是否為 loading。
import { Outlet, useNavigation } from "react-router-dom";import MainNavigation from "../components/MainNavigation";const RootLayout = () => {  const navigation = useNavigation();  return (    <div>      <MainNavigation />      {navigation.state === "loading" ? <h1>Loading..</h1> : <Outlet />}    </div>  );};export default RootLayout;Error Handling & Custom Errors
我們當然也可以客製化錯誤訊息,將 Products.jsx 的程式碼改成以下:
import { useLoaderData } from "react-router-dom";import ProductsList from "../components/ProductsList";const ProductsPage = () => {  const data = useLoaderData();  if (data.isError) {    return <h1>{data.message}</h1>;  }  return <ProductsList />;};export default ProductsPage;export const loader = async () => {  const response = await fetch("https://aaaaaaadummyjson.com/products?limit=5");  if (!response.ok) {    return {      isError: true,      message: "Something went wrong!!!",    };  } else {    const data = await response.json();    return data.products;  }};到 Products 頁面就會看到我們的錯誤訊息:

Error Handling & Custom Errors (Bubble Up)
上述的客製化錯誤訊息算是比較偷懶的做法,所以我們可以使用較正式的做法,先將 Products.jsx 的程式碼改為以下:
import { useLoaderData } from "react-router-dom";import ProductsList from "../components/ProductsList";const ProductsPage = () => {  const data = useLoaderData();  return <ProductsList />;};export default ProductsPage;export const loader = async () => {  const response = await fetch("https://aaaaaaadummyjson.com/products?limit=5");  if (!response.ok) {    throw { message: "Something went wrong!!!" };  } else {    const data = await response.json();    return data.products;  }};再次來到 Products 頁面後,會發現這次出現的錯誤訊息是之前在 Error.jsx 定義的訊息。
import MainNavigation from "../components/MainNavigation";const ErrorPage = () => {  return (    <>      <MainNavigation />      <div>        <h1>An error occurred!!</h1>        <p>Could not find this page</p>      </div>    </>  );};export default ErrorPage;
這是因為我們在 Router 中有定義 errorElement,當錯誤發生時,React Router Dom 會先檢查在該 path 底下有沒有定義 errorElement,沒有的話就往上尋找(Bubble Up),找到之後就渲染出 errorElement 的畫面。
const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    children: [      { index: true, element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,        loader: ProductsLoader,      },      { path: "products/:productId", element: <ProductDetailPage /> },    ],  },]);useRouteError
現在可以再更進階一點,根據 status code 給予不同的錯誤訊息,將 Products.jsx 程式碼改為以下:
import { useLoaderData } from "react-router-dom";import ProductsList from "../components/ProductsList";const ProductsPage = () => {  const data = useLoaderData();  return <ProductsList />;};export default ProductsPage;export const loader = async () => {  const response = await fetch("https://aaaaaaadummyjson.com/products?limit=5");  if (!response.ok) {    throw new Response(JSON.stringify({ message: "Something went wrong!!!" }), {      status: 500,    });  } else {    const data = await response.json();    return data.products;  }};在 Error.jsx 中,使用 useRouteError 將錯誤資訊取出來,並做判斷來顯示不同的文字。
import { useRouteError } from "react-router-dom";import MainNavigation from "../components/MainNavigation";const ErrorPage = () => {  const error = useRouteError();  let title = "An error occurred!!";  let message = "Could not find this page";  if (error.status === 500) {    message = JSON.parse(error.data).message;  }  if (error.status === 404) {    title = "Not Found!";    message = "Could not find this page";  }  return (    <>      <MainNavigation />      <div>        <h1>{title}</h1>        <p>{message}</p>      </div>    </>  );};export default ErrorPage;json
不知道你會不會覺得上述的 Error Handling 方式都有些麻煩,要使用 Response 建立一個物件,並使用 JSON.stringify 將資料轉成 Json 字串,拿資料的時候又要利用 JSON.parse 將資料轉換回來。
所以 React Router Dom 提供了 json 方法讓我們能更方便的處理 Error 訊息。
將 Products.jsx 和 Error.jsx 中的程式碼改為以下即可:
import { useLoaderData, json } from "react-router-dom";import ProductsList from "../components/ProductsList";const ProductsPage = () => {  const data = useLoaderData();  return <ProductsList />;};export default ProductsPage;export const loader = async () => {  const response = await fetch("https://aaaaaaadummyjson.com/products?limit=5");  if (!response.ok) {    throw json({ message: "Something went wrong!!!" }, { status: 500 });  } else {    const data = await response.json();    return data.products;  }};import { useRouteError } from "react-router-dom";import MainNavigation from "../components/MainNavigation";const ErrorPage = () => {  const error = useRouteError();  let title = "An error occurred!!";  let message = "Could not find this page";  if (error.status === 500) {    message = error.data.message;  }  if (error.status === 404) {    title = "Not Found!";    message = "Could not find this page";  }  return (    <>      <MainNavigation />      <div>        <h1>{title}</h1>        <p>{message}</p>      </div>    </>  );};export default ErrorPage;Dynamic Routes & loader
現在先將 Products.jsx 、 ProductsList.jsx 的程式碼改成以下:
import { useLoaderData, json } from "react-router-dom";import ProductsList from "../components/ProductsList";const ProductsPage = () => {  const data = useLoaderData();  return <ProductsList data={data} />;};export default ProductsPage;export const loader = async () => {  const response = await fetch("https://dummyjson.com/products?limit=5");  if (!response.ok) {    throw json({ message: "Something went wrong!!!" }, { status: 500 });  } else {    const data = await response.json();    return data.products;  }};現在可以使用 id 來找到單一個產品的詳細資料了。
import { Link } from "react-router-dom";const ProductsList = ({ data }) => {  return (    <div>      <h1>Products List</h1>      {data.map((product) => (        <div key={product.id}>          <p>{product.title}</p>          <Link to={"/products/" + product.id}>            <img width={100} src={product.images[0]} />          </Link>        </div>      ))}    </div>  );};export default ProductsList;接著在 components 資料夾底下 新增 ProductItem.jsx,並修改 ProductDetail.jsx 內的程式碼:
const ProductItem = () => {  return <div>ProductItem</div>;};export default ProductItem;當我們使用 loader 時,它會自帶兩個參數,一個是 request 另一個則是 params,我們可以使用 params 來取得 id,跟 useParams 的用途是一樣的。
這邊也直接將取得的 data 傳遞至 ProductItem Component。
import { useLoaderData, json } from "react-router-dom";import ProductItem from "../components/ProductItem";const ProductDetailPage = () => {  const data = useLoaderData();  return <ProductItem data={data} />;};export default ProductDetailPage;export const loader = async ({ request, params }) => {  const id = params.productId;  const response = await fetch(`https://dummyjson.com/products/${id}`);  if (!response.ok) {    throw json({ message: "Something went wrong!!!" }, { status: 500 });  } else {    const data = await response.json();    return data;  }};別忘記定義完 loader 後,也要在該 path 引入,所以回到 App.jsx,將 loader 新增至 path: "products/:productId"。
import { createBrowserRouter, RouterProvider } from "react-router-dom";import "./App.css";import ErrorPage from "./pages/Error";import HomePage from "./pages/Home";import ProductDetailPage, {  loader as ProductDetailLoader,} from "./pages/ProductDetail";import ProductsPage, { loader as ProductsLoader } from "./pages/Products";import RootLayout from "./pages/Root";const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    children: [      { index: true, element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,        loader: ProductsLoader,      },      {        path: "products/:productId",        element: <ProductDetailPage />,        loader: ProductDetailLoader,      },    ],  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;現在可以可以修改 ProductItem.jsx 內的程式碼了:
import { Link } from "react-router-dom";const ProductItem = ({ data }) => {  return (    <div>      <p>{data.title}</p>      <img width={100} src={data.images[0]} />      <br />      <Link to=".." relative="path">        Back      </Link>    </div>  );};export default ProductItem;useRouteLoaderData
useRouteLoaderData 是讓 Child Route 可以去使用 Parent Route 所定義的 loader。
先在 App.jsx 的 Parent Route 的地方新增 id 和 loader:
import { createBrowserRouter, RouterProvider } from "react-router-dom";import "./App.css";import ErrorPage from "./pages/Error";import HomePage from "./pages/Home";import ProductDetailPage, {  loader as ProductDetailLoader,} from "./pages/ProductDetail";import ProductsPage, { loader as ProductsLoader } from "./pages/Products";import RootLayout from "./pages/Root";const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    id: "root",    loader: () => {      return "Hello World";    },    children: [      { index: true, element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,        loader: ProductsLoader,      },      {        path: "products/:productId",        element: <ProductDetailPage />,        loader: ProductDetailLoader,      },    ],  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;接著我們可以在底下的 Child Route 使用 useRouteLoaderData,將 Home.jsx 程式碼改為以下,即可取得 Parent Route loader 回傳的值:
import { Link, useRouteLoaderData } from "react-router-dom";const HomePage = () => {  const data = useRouteLoaderData("root"); // 依靠 id 取得 root loader 的值  console.log(data); // Hello World  return (    <div>      <h1>Home</h1>      <p>        Go to <Link to="/products">products</Link>      </p>    </div>  );};export default HomePage;action
如果我們要提交表單的資料,則可以使用 action,action 和 loader 非常類似,但 action 可以接收表單內的資料,所以 action 通常是拿來發 Post Request,action 和 loader 一樣,都必須在 path 中去定義。
要讓 action 能夠接收表單資料,得先引入 React Router Dom 的 Form,所以先來新增兩個檔案,ProductForm.jsx 和 ProductAction.jsx 。
import { Form } from "react-router-dom";const ProductForm = () => {  return (    <Form method="post">      <p>        <label htmlFor="title">Title</label>        <input type="text" id="title" name="title" />      </p>      <p>        <label htmlFor="price">Price</label>        <input type="text" id="price" name="price" />      </p>      <p>        <label htmlFor="description">Description</label>        <textarea id="description" name="description" />      </p>      <button type="submit">Submit</button>    </Form>  );};export default ProductForm;我們也可以引入 redirect,假設 Post Request 發送成功的話,就導向至主頁面。
import { redirect } from "react-router-dom";import ProductForm from "../components/ProductForm";const ProductActionPage = () => {  return <ProductForm />;};export default ProductActionPage;export const action = async ({ request, params }) => {  const data = await request.formData(); // 接收 Form 表單裡面的資料  const productData = {    title: data.get("title"),    price: data.get("price"),    description: data.get("description"),  };  const response = await fetch("https://dummyjson.com/products/add", {    method: "POST",    headers: { "Content-Type": "application/json" },    body: JSON.stringify(productData),  });  if (!response.ok) {    throw json({ message: "Something went wrong!!!" }, { status: 500 });  }  return redirect("/");};定義完 action 以後,將 ProductAction.jsx 的 path 定義一下,同時將 action 傳入:
import { createBrowserRouter, RouterProvider } from "react-router-dom";import "./App.css";import ErrorPage from "./pages/Error";import HomePage from "./pages/Home";import ProductDetailPage, {  loader as ProductDetailLoader,} from "./pages/ProductDetail";import ProductActionPage, {  action as ProductAction,} from "./pages/ProductAction";import ProductsPage, { loader as ProductsLoader } from "./pages/Products";import RootLayout from "./pages/Root";const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    id: "root",    loader: () => {      return "Hello World";    },    children: [      { index: true, element: <HomePage /> },      {        path: "products",        element: <ProductsPage />,        loader: ProductsLoader,      },      {        path: "products/:productId",        element: <ProductDetailPage />,        loader: ProductDetailLoader,      },      {        path: "products/add",        element: <ProductActionPage />,        action: ProductAction,      },    ],  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;Home.jsx 中的程式碼也要修改一下,才能進入到 products/add:
import { Link, useRouteLoaderData } from "react-router-dom";const HomePage = () => {  const data = useRouteLoaderData("root");  console.log(data);  return (    <div>      <h1>Home</h1>      <p>        Go to <Link to="/products">products</Link>      </p>      <p>        Go to <Link to="/products/add">add product</Link>      </p>    </div>  );};export default HomePage;Form Submitting
前面提到 useNavigation 時,有提到 useNavigation 能取得當前的 state,而當我們的 Form 表單正在送出處理時,state 會是 submitting,所以我們也可以利用這一個特性來告知使用者目前表單的處理狀況。
將 ProductForm.jsx 的程式碼修改為以下:
import { Form, useNavigation } from "react-router-dom";const ProductForm = () => {  const navigation = useNavigation();  const isSubmitting = navigation.state === "submitting";  return (    <Form method="post">      <p>        <label htmlFor="title">Title</label>        <input type="text" id="title" name="title" />      </p>      <p>        <label htmlFor="price">Price</label>        <input type="text" id="price" name="price" />      </p>      <p>        <label htmlFor="description">Description</label>        <textarea id="description" name="description" />      </p>      <button type="submit">{isSubmitting ? "Submit..." : "Submit"}</button>    </Form>  );};export default ProductForm;defer & Await
要 Demo defer 和 Await的話,我們需要再新增兩個檔案,ProductDeferTest.jsx 和 ProductRoot.jsx。
const ProductDeferTest = () => {  return <div>ProductDeferTest</div>;};export default ProductDeferTest;import { Outlet } from "react-router-dom";import ProductDeferTest from "../components/ProductDeferTest";const ProductRoot = () => {  return (    <>      <ProductDeferTest />      <Outlet />    </>  );};export default ProductRoot;之後再將 App.jsx 內的 Router 更改一下:
import { createBrowserRouter, RouterProvider } from "react-router-dom";import "./App.css";import ErrorPage from "./pages/Error";import HomePage from "./pages/Home";import ProductDetailPage, {  loader as ProductDetailLoader,} from "./pages/ProductDetail";import ProductActionPage, {  action as ProductAction,} from "./pages/ProductAction";import ProductsPage, { loader as ProductsLoader } from "./pages/Products";import RootLayout from "./pages/Root";import ProductRoot from "./pages/ProductRoot";const router = createBrowserRouter([  {    path: "/",    element: <RootLayout />,    errorElement: <ErrorPage />,    id: "root",    loader: () => {      return "Hello World";    },    children: [      { index: true, element: <HomePage /> },      {        path: "products",        element: <ProductRoot />,        children: [          {            index: true,            element: <ProductsPage />,            loader: ProductsLoader,          },          {            path: ":productId",            element: <ProductDetailPage />,            loader: ProductDetailLoader,          },          {            path: "add",            element: <ProductActionPage />,            action: ProductAction,          },        ],      },    ],  },]);function App() {  return (    <div className="App">      <RouterProvider router={router} />    </div>  );}export default App;現在進入到 path 為 products 底下的頁面,都會看到 ProductRoot.jsx 內的文字 ProductDeferTest。
但不知道你有沒有發現一個問題,我們的 ProductDeferTest 文字,是等 loader 處理完 API 資料,並顯示在畫面上後才出現,如果今天 ProductRoot.jsx 內要顯示的畫面對使用者來說是重要的,這樣的使用者體驗就不太好。

以 Products.jsx 為例,我們可以先將原本的 loader 程式碼搬移出去,建立另外一個 Function,名為 loadProducts,並在原本的 loader return defer 並執行 loadProducts。
import { useLoaderData, json, defer } from "react-router-dom";import ProductsList from "../components/ProductsList";const ProductsPage = () => {  const data = useLoaderData();  return <ProductsList data={data} />;};export default ProductsPage;const loadProducts = async () => {  const response = await fetch("https://dummyjson.com/products?limit=5");  if (!response.ok) {    throw json({ message: "Something went wrong!!!" }, { status: 500 });  } else {    const data = await response.json();    return data.products;  }};export const loader = async () => {  return defer({    data: loadProducts(),  });};defer 內能執行多個 Promise Function,只要給不同的 key 即可,以上述的例子來看,我們的 loadProducts() 對應的 key 為 data,所以在使用 useLoaderData 時,需要將 data 解構出來做使用。
接著需要搭配 Suspense 和 Await,讓資料讀取的時候能顯示文字在畫面上,等到資料讀取完畢後,才會顯示 ProductsList Component 裡面的內容。
import { Suspense } from "react";import { useLoaderData, json, defer, Await } from "react-router-dom";import ProductsList from "../components/ProductsList";const ProductsPage = () => {  const { data } = useLoaderData();  return (    <Suspense fallback={<p style={{ textAlign: "center" }}>Loading...</p>}>      <Await resolve={data}>        {(loadProducts) => <ProductsList data={loadProducts} />}      </Await>    </Suspense>  );};export default ProductsPage;const loadProducts = async () => {  const response = await fetch("https://dummyjson.com/products?limit=5");  if (!response.ok) {    throw json({ message: "Something went wrong!!!" }, { status: 500 });  } else {    const data = await response.json();    return data.products;  }};export const loader = async () => {  return defer({    data: loadProducts(),  });};