測驗:React Router 巢狀路由與 Outlet 佈局
共 5 題,點選答案後會立即顯示結果
1. 在 React Router 中,<Outlet /> 元件的主要功能是什麼?
2. 觀察以下路由設定,當使用者訪問 /dashboard 時,會渲染哪個元件?
3. 在巢狀路由的設定中,index: true 代表什麼意思?
4. 在 Layout 元件中,如果想要傳遞資料給子路由,應該使用什麼方式?
5. 在子頁面元件中,要如何取得 Layout 透過 Outlet 傳遞的資料?
前言
在前一篇文章中,我們學會了使用動態路由處理參數。但當你開始建構較複雜的應用程式時,會發現很多頁面其實共用相同的版面結構——例如頂部導航列、側邊欄等。這時候如果每個頁面都重複寫一遍這些共用元件,不但麻煩,維護起來也是惡夢。
巢狀路由(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)這段程式碼告訴我們:
- 父路由
/dashboard對應<DashboardLayout />元件 children陣列定義了子路由path: ""是索引路由,當訪問/dashboard時顯示- 相對路徑:子路由的 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)閱讀重點:
<Outlet />標記了子路由的渲染位置<Outlet />以外的部分都是共用的版面- 當路由切換時,只有
<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: true 是 path: "" 的另一種寫法,表示「當訪問父路由時,顯示這個元件」。這讓設定更加明確易讀。
多層巢狀: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)重點觀察:
- RootLayout 的
<Outlet />會渲染 AdminLayout - AdminLayout 的
<Outlet />會渲染實際頁面內容 - 形成了一個元件嵌套鏈
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)閱讀重點:
<Outlet context={...} />:佈局透過 context prop 傳遞資料useOutletContext():子頁面透過這個 Hook 接收資料- 可以傳遞任何值:物件、陣列、函式都可以
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)閱讀程式碼的檢查清單
當你在專案中看到巢狀路由相關的程式碼時,可以依序檢查:
- 路由設定檔
- 找到
children陣列,了解路由層級關係 - 注意
index: true或path: ""的索引路由 - 觀察路徑如何組合(父路徑 + 子路徑)
- 找到
- Layout 元件
- 找到
<Outlet />,這是子頁面渲染的位置 - 確認
<Outlet />以外的內容都是共用部分 - 檢查是否有
context屬性傳遞資料
- 找到
- 子頁面元件
- 如果使用了
useOutletContext(),回頭看佈局傳了什麼 - 子頁面不需要處理共用版面,只需專注自己的內容
- 如果使用了
重點整理
| 概念 | 說明 |
|---|---|
| 巢狀路由 | 在 children 陣列中定義子路由,形成層級結構 |
<Outlet /> |
標記子路由渲染位置的元件,像是版面的「插槽」 |
index: true |
索引路由,當訪問父路徑時顯示的預設內容 |
| Layout 元件 | 包含 <Outlet /> 的元件,定義共用版面結構 |
useOutletContext() |
讓子頁面取得佈局傳遞的資料 |
下一步
現在你已經掌握了巢狀路由和佈局的概念。在下一篇文章中,我們將學習路由守衛和導航控制,包括如何實作需要登入才能訪問的保護路由,以及如何在程式中控制頁面跳轉。
進階測驗:React Router 巢狀路由與 Outlet 佈局
共 5 題,包含情境題與錯誤診斷題。
1. 你正在開發一個電商後台,需要讓所有管理頁面共用側邊欄和頂部導航。情境題
你希望訪問 /admin 時顯示儀表板,訪問 /admin/products 時顯示產品列表。以下哪個路由設定最適合?
2. 小明的程式碼中,子頁面無法取得 Layout 傳遞的使用者資訊。錯誤診斷
最可能的問題是什麼?
3. 你需要設計一個應用程式,公開頁面不需要登入,但 /app 底下的頁面需要認證。情境題
以下哪種路由架構最符合這個需求?
4. 以下是一個多層巢狀路由的設定,當使用者訪問 /admin/orders/123 時,元件會如何渲染?情境題
5. 小華的子路由設定有問題,訪問 /dashboard/users 時出現 404 錯誤。錯誤診斷
最可能的錯誤原因是什麼?