【React Router 教學】#04 巢狀路由與 Outlet 佈局

測驗:React Router 巢狀路由與 Outlet 佈局

共 5 題,點選答案後會立即顯示結果

1. 在 React Router 中,<Outlet /> 元件的主要功能是什麼?

  • A. 用來定義路由的路徑
  • B. 標記子路由應該渲染的位置
  • C. 用來建立導航連結
  • D. 處理路由參數的傳遞

2. 觀察以下路由設定,當使用者訪問 /dashboard 時,會渲染哪個元件?

{ path: “/dashboard”, element: <DashboardLayout />, children: [ { index: true, element: <DashboardHome /> }, { path: “users”, element: <UserList /> } ] }
  • A. 只有 DashboardLayout
  • B. 只有 DashboardHome
  • C. DashboardLayout 搭配 DashboardHome
  • D. DashboardLayout 搭配 UserList

3. 在巢狀路由的設定中,index: true 代表什麼意思?

  • A. 這是路由的根路徑
  • B. 當訪問父路由時顯示的預設內容
  • C. 這是第一個載入的元件
  • D. 這是唯一會被渲染的元件

4. 在 Layout 元件中,如果想要傳遞資料給子路由,應該使用什麼方式?

  • A. 透過 <Outlet context={...} /> 傳遞
  • B. 透過 props 直接傳遞
  • C. 透過 <Link state={...} /> 傳遞
  • D. 透過 URL 參數傳遞

5. 在子頁面元件中,要如何取得 Layout 透過 Outlet 傳遞的資料?

  • A. 使用 useParams()
  • B. 使用 useNavigate()
  • C. 使用 useOutletContext()
  • D. 使用 useLocation()

前言

在前一篇文章中,我們學會了使用動態路由處理參數。但當你開始建構較複雜的應用程式時,會發現很多頁面其實共用相同的版面結構——例如頂部導航列、側邊欄等。這時候如果每個頁面都重複寫一遍這些共用元件,不但麻煩,維護起來也是惡夢。

巢狀路由(Nested Routes)就是解決這個問題的利器。透過 React Router 的巢狀路由功能,你可以建立「佈局元件」,讓子頁面自動嵌入到指定位置,達到程式碼重用的目的。

巢狀路由的基本概念

什麼是巢狀路由?

巢狀路由就是「路由裡面還有路由」。想像一個後台管理系統:

/dashboard           → 儀表板首頁
/dashboard/users     → 使用者管理
/dashboard/products  → 產品管理
/dashboard/settings  → 設定頁面

這些頁面都在 /dashboard 底下,而且可能共用相同的側邊欄和頂部導航。這就是巢狀路由的典型應用場景。

路由設定的寫法

當你看到以下的路由設定時,重點是觀察 children 屬性:

const router = createBrowserRouter([
  {
    path: "/dashboard",
    element: <DashboardLayout />,
    children: [
      { path: "", element: <DashboardHome /> },
      { path: "users", element: <UserList /> },
      { path: "products", element: <ProductList /> }
    ]
  }
]);
Code language: JavaScript (javascript)

這段程式碼告訴我們:

  1. 父路由 /dashboard 對應 <DashboardLayout /> 元件
  2. children 陣列定義了子路由
  3. path: "" 是索引路由,當訪問 /dashboard 時顯示
  4. 相對路徑:子路由的 path 不需要加 /,會自動接在父路由後面

Outlet 元件:子路由的渲染位置

Outlet 是什麼?

<Outlet /> 是 React Router 提供的元件,用來標記「子路由應該渲染在哪裡」。你可以把它想像成一個「插槽」或「佔位符」。

閱讀 Layout 元件

當你看到一個 Layout 元件時,找 <Outlet /> 就知道子頁面會出現在哪:

import { Outlet, Link } from "react-router-dom";

function DashboardLayout() {
  return (
    <div className="dashboard">
      {/* 這是共用的頂部導航 */}
      <header>
        <h1>後台管理系統</h1>
        <nav>
          <Link to="/dashboard">首頁</Link>
          <Link to="/dashboard/users">使用者</Link>
          <Link to="/dashboard/products">產品</Link>
        </nav>
      </header>

      <div className="main-content">
        {/* 這是共用的側邊欄 */}
        <aside>
          <ul>
            <li><Link to="/dashboard/settings">設定</Link></li>
            <li><Link to="/dashboard/profile">個人資料</Link></li>
          </ul>
        </aside>

        {/* 子路由會渲染在這裡 */}
        <main>
          <Outlet />
        </main>
      </div>
    </div>
  );
}
Code language: JavaScript (javascript)

閱讀重點:

  1. <Outlet /> 標記了子路由的渲染位置
  2. <Outlet /> 以外的部分都是共用的版面
  3. 當路由切換時,只有 <Outlet /> 內的內容會改變

視覺化理解

┌─────────────────────────────────────────┐
│  Header(共用)                          │
├──────────┬──────────────────────────────┤
│          │                              │
│  Sidebar │     <Outlet />               │
│  (共用) │     這裡放子頁面內容           │
│          │                              │
└──────────┴──────────────────────────────┘
Code language: HTML, XML (xml)

實際案例:電商後台佈局

完整的路由設定

import { createBrowserRouter, RouterProvider } from "react-router-dom";

// 路由設定
const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    children: [
      { index: true, element: <HomePage /> },
      { path: "about", element: <AboutPage /> },
      {
        path: "admin",
        element: <AdminLayout />,
        children: [
          { index: true, element: <AdminDashboard /> },
          { path: "orders", element: <OrderList /> },
          { path: "orders/:orderId", element: <OrderDetail /> },
          { path: "products", element: <ProductManagement /> }
        ]
      }
    ]
  }
]);

function App() {
  return <RouterProvider router={router} />;
}
Code language: JavaScript (javascript)

閱讀這段設定時,注意以下重點:

路徑 渲染的元件組合
/ RootLayout → HomePage
/about RootLayout → AboutPage
/admin RootLayout → AdminLayout → AdminDashboard
/admin/orders RootLayout → AdminLayout → OrderList
/admin/orders/123 RootLayout → AdminLayout → OrderDetail

index 路由

{ index: true, element: <AdminDashboard /> }
Code language: CSS (css)

index: truepath: "" 的另一種寫法,表示「當訪問父路由時,顯示這個元件」。這讓設定更加明確易讀。

多層巢狀:RootLayout 與子佈局

兩層佈局的結構

// 最外層佈局:共用導航列和頁尾
function RootLayout() {
  return (
    <div>
      <nav>
        <Link to="/">首頁</Link>
        <Link to="/admin">後台</Link>
      </nav>

      {/* 這裡會渲染 HomePage、AboutPage 或 AdminLayout */}
      <Outlet />

      <footer>© 2024 My Company</footer>
    </div>
  );
}

// 後台專用佈局:側邊選單
function AdminLayout() {
  return (
    <div className="admin-container">
      <aside className="admin-sidebar">
        <Link to="/admin">儀表板</Link>
        <Link to="/admin/orders">訂單</Link>
        <Link to="/admin/products">產品</Link>
      </aside>

      {/* 這裡會渲染 AdminDashboard、OrderList 等 */}
      <main className="admin-content">
        <Outlet />
      </main>
    </div>
  );
}
Code language: JavaScript (javascript)

重點觀察:

  1. RootLayout<Outlet /> 會渲染 AdminLayout
  2. AdminLayout<Outlet /> 會渲染實際頁面內容
  3. 形成了一個元件嵌套鏈

useOutletContext:佈局與子頁面的資料傳遞

為什麼需要 useOutletContext?

有時候佈局元件持有一些狀態(例如使用者資訊),子頁面需要存取這些資料。useOutletContext 讓佈局可以傳遞資料給子路由。

設定 context

import { Outlet } from "react-router-dom";
import { useState } from "react";

function AdminLayout() {
  const [user] = useState({ name: "Admin", role: "manager" });
  const [sidebarOpen, setSidebarOpen] = useState(true);

  // 透過 context 傳遞資料給子路由
  return (
    <div>
      <aside className={sidebarOpen ? "open" : "closed"}>
        {/* 側邊欄內容 */}
      </aside>
      <main>
        <Outlet context={{ user, sidebarOpen, setSidebarOpen }} />
      </main>
    </div>
  );
}
Code language: JavaScript (javascript)

在子頁面使用 context

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

function OrderList() {
  // 從佈局取得傳遞的資料
  const { user, sidebarOpen } = useOutletContext();

  return (
    <div>
      <h1>訂單列表</h1>
      <p>目前使用者:{user.name}({user.role})</p>
      <p>側邊欄狀態:{sidebarOpen ? "展開" : "收合"}</p>
      {/* 訂單列表內容 */}
    </div>
  );
}
Code language: JavaScript (javascript)

閱讀重點:

  1. <Outlet context={...} />:佈局透過 context prop 傳遞資料
  2. useOutletContext():子頁面透過這個 Hook 接收資料
  3. 可以傳遞任何值:物件、陣列、函式都可以

TypeScript 型別定義

在 TypeScript 專案中,你可能會看到這樣的型別定義:

// 定義 context 的型別
type AdminContextType = {
  user: { name: string; role: string };
  sidebarOpen: boolean;
  setSidebarOpen: (open: boolean) => void;
};

// 在子頁面使用時指定型別
function OrderList() {
  const { user, sidebarOpen } = useOutletContext<AdminContextType>();
  // ...
}
Code language: JavaScript (javascript)

常見的佈局模式

模式一:認證佈局

const router = createBrowserRouter([
  {
    path: "/",
    element: <PublicLayout />,    // 公開頁面佈局
    children: [
      { index: true, element: <HomePage /> },
      { path: "login", element: <LoginPage /> }
    ]
  },
  {
    path: "/app",
    element: <AuthenticatedLayout />,  // 需要登入的佈局
    children: [
      { index: true, element: <Dashboard /> },
      { path: "profile", element: <Profile /> }
    ]
  }
]);
Code language: JavaScript (javascript)

模式二:三欄佈局

function ThreeColumnLayout() {
  return (
    <div className="three-column">
      <header>網站標題</header>
      <div className="content-area">
        <nav className="left-nav">左側導航</nav>
        <main>
          <Outlet />  {/* 主要內容 */}
        </main>
        <aside className="right-sidebar">右側邊欄</aside>
      </div>
      <footer>頁尾</footer>
    </div>
  );
}
Code language: JavaScript (javascript)

模式三:標籤頁佈局

function TabLayout() {
  return (
    <div>
      <nav className="tabs">
        <NavLink to="/settings/general">一般</NavLink>
        <NavLink to="/settings/security">安全</NavLink>
        <NavLink to="/settings/notifications">通知</NavLink>
      </nav>
      <div className="tab-content">
        <Outlet />
      </div>
    </div>
  );
}
Code language: JavaScript (javascript)

閱讀程式碼的檢查清單

當你在專案中看到巢狀路由相關的程式碼時,可以依序檢查:

  1. 路由設定檔
    • 找到 children 陣列,了解路由層級關係
    • 注意 index: truepath: "" 的索引路由
    • 觀察路徑如何組合(父路徑 + 子路徑)
  2. Layout 元件
    • 找到 <Outlet />,這是子頁面渲染的位置
    • 確認 <Outlet /> 以外的內容都是共用部分
    • 檢查是否有 context 屬性傳遞資料
  3. 子頁面元件
    • 如果使用了 useOutletContext(),回頭看佈局傳了什麼
    • 子頁面不需要處理共用版面,只需專注自己的內容

重點整理

概念 說明
巢狀路由 children 陣列中定義子路由,形成層級結構
<Outlet /> 標記子路由渲染位置的元件,像是版面的「插槽」
index: true 索引路由,當訪問父路徑時顯示的預設內容
Layout 元件 包含 <Outlet /> 的元件,定義共用版面結構
useOutletContext() 讓子頁面取得佈局傳遞的資料

下一步

現在你已經掌握了巢狀路由和佈局的概念。在下一篇文章中,我們將學習路由守衛和導航控制,包括如何實作需要登入才能訪問的保護路由,以及如何在程式中控制頁面跳轉。

進階測驗:React Router 巢狀路由與 Outlet 佈局

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發一個電商後台,需要讓所有管理頁面共用側邊欄和頂部導航。情境題

你希望訪問 /admin 時顯示儀表板,訪問 /admin/products 時顯示產品列表。以下哪個路由設定最適合?

  • A. 將 AdminLayout、Dashboard、ProductList 都放在同一層 children 中
  • B. AdminLayout 作為父路由的 element,Dashboard 設為 index 路由,ProductList 設為 path: “products”
  • C. 在每個頁面元件中都 import 並使用 AdminLayout
  • D. 建立兩個獨立的路由,分別設定 /admin 和 /admin/products

2. 小明的程式碼中,子頁面無法取得 Layout 傳遞的使用者資訊。錯誤診斷

// AdminLayout.jsx function AdminLayout() { const [user] = useState({ name: “Admin” }); return ( <div> <Sidebar /> <Outlet /> </div> ); } // OrderList.jsx function OrderList() { const { user } = useOutletContext(); return <div>使用者:{user.name}</div>; }

最可能的問題是什麼?

  • A. useOutletContext 必須在 useEffect 中呼叫
  • B. 需要在 OrderList 中使用 useState 來接收 user
  • C. AdminLayout 的 Outlet 沒有設定 context 屬性來傳遞 user
  • D. user 物件需要透過 JSON.stringify 轉換後才能傳遞

3. 你需要設計一個應用程式,公開頁面不需要登入,但 /app 底下的頁面需要認證。情境題

以下哪種路由架構最符合這個需求?

  • A. 所有路由放在同一個 children 陣列,在每個需要認證的元件內檢查登入狀態
  • B. 使用單一 RootLayout,透過 URL 判斷是否顯示登入表單
  • C. 只建立一個路由,動態載入不同的元件
  • D. 建立兩個獨立的父路由:PublicLayout 用於公開頁面,AuthenticatedLayout 用於需要登入的頁面

4. 以下是一個多層巢狀路由的設定,當使用者訪問 /admin/orders/123 時,元件會如何渲染?情境題

const router = createBrowserRouter([ { path: “/”, element: <RootLayout />, children: [ { path: “admin”, element: <AdminLayout />, children: [ { path: “orders/:orderId”, element: <OrderDetail /> } ] } ] } ]);
  • A. 只渲染 OrderDetail
  • B. RootLayout 的 Outlet 渲染 AdminLayout,AdminLayout 的 Outlet 渲染 OrderDetail
  • C. RootLayout 和 AdminLayout 同時渲染在頁面上,OrderDetail 獨立顯示
  • D. AdminLayout 的 Outlet 渲染 RootLayout,RootLayout 的 Outlet 渲染 OrderDetail

5. 小華的子路由設定有問題,訪問 /dashboard/users 時出現 404 錯誤。錯誤診斷

const router = createBrowserRouter([ { path: “/dashboard”, element: <DashboardLayout />, children: [ { path: “/users”, element: <UserList /> }, { path: “/products”, element: <ProductList /> } ] } ]);

最可能的錯誤原因是什麼?

  • A. 子路由的 path 不應該以 “/” 開頭,應該使用相對路徑如 “users”
  • B. 缺少 index 路由的設定
  • C. DashboardLayout 沒有正確匯出
  • D. createBrowserRouter 不支援 children 屬性

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *