React Router 路由套件
React 本身只是一個 UI 函式庫,並不包含路由 (Routing) 功能。在開發單頁式應用程式 (SPA, Single Page Application) 時,我們需要一個工具來管理 URL 與元件之間的對應關係,而 React Router 就是 React 生態系中標準的路由解決方案。
安裝
React Router 針對不同平台有不同的套件,在網頁開發中,我們使用 react-router-dom:
npm install react-router-dom
基本設定 (Basic Setup)
要在 React 中使用路由,首先需要在應用程式的最外層包裹一個 Router 元件,通常使用 <BrowserRouter>。
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);
接著在 App.jsx 中設定路由規則。我們使用 <Routes> 和 <Route> 來定義。
// App.jsx
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';
function App() {
return (
<div className="app">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* 404 Not Found 路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
);
}
導航 (Navigation)
<Link> - 基本連結
在 SPA 中,我們不使用 HTML 的 <a> 標籤來跳轉頁面,因為那會導致瀏覽器重新整理整個頁面。取而代之的是使用 <Link> 元件。
import { Link } from 'react-router-dom';
function Navbar() {
return (
<nav>
<Link to="/">首頁</Link>
<Link to="/about">關於我們</Link>
</nav>
);
}
<NavLink> - 帶有狀態的連結
<NavLink> 是特殊版本的 <Link>,它可以在目前的 URL 與 to 屬性相符時,自動加入 active class 或 style,非常適合用在導航列。
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav>
<NavLink to="/" className={({ isActive }) => (isActive ? 'active-link' : '')}>
首頁
</NavLink>
<NavLink to="/about" style={({ isActive }) => ({ color: isActive ? 'red' : 'black' })}>
關於我們
</NavLink>
</nav>
);
}
useNavigate - 程式化導航
如果你需要在程式碼中(例如表單提交後)進行跳轉,可以使用 useNavigate Hook。
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
// ... 執行登入邏輯
await login();
// 登入成功後跳轉到首頁
navigate('/');
// 或者跳轉並取代目前的歷史紀錄 (replace: true)
// navigate('/dashboard', { replace: true })
// 也可以用來回上一頁
// navigate(-1)
}
return <form onSubmit={handleSubmit}>...</form>;
}
動態路由參數 (URL Parameters)
當你需要根據 URL 中的參數來顯示不同內容(例如 /products/123)時,可以在路徑中使用冒號 : 來定義參數,並使用 useParams Hook 來取得。
定義路由:
<Route path="/products/:id" element={<ProductDetail />} />
取得參數:
// ProductDetail.jsx
import { useParams } from 'react-router-dom';
function ProductDetail() {
const { id } = useParams();
return <h1>正在查看商品 ID: {id}</h1>;
}
查詢參數 (Query String)
對於像 /search?q=react 這樣的查詢參數,我們使用 useSearchParams Hook,它的用法很像 useState。
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q'); // 取得 'react'
return (
<div>
<input value={query || ''} onChange={(e) => setSearchParams({ q: e.target.value })} />
</div>
);
}
巢狀路由 (Nested Routes)
React Router 最強大的功能之一就是巢狀路由。這允許你的 UI 結構與 URL 結構相匹配,父路由可以共享版面配置 (Layout) 給子路由。
1. 定義巢狀結構
// App.jsx
<Route path="/dashboard" element={<DashboardLayout />}>
{/* path="" 代表預設子路由 (/dashboard) */}
<Route index element={<DashboardHome />} />
{/* /dashboard/settings */}
<Route path="settings" element={<Settings />} />
{/* /dashboard/profile */}
<Route path="profile" element={<Profile />} />
</Route>
2. 使用 <Outlet>
在父元件中,使用 <Outlet> 來指定子路由渲染的位置。
// DashboardLayout.jsx
import { Outlet, Link } from 'react-router-dom';
function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<Link to="/dashboard">總覽</Link>
<Link to="/dashboard/settings">設定</Link>
<Link to="/dashboard/profile">個人資料</Link>
</aside>
<main>
{/* 子路由的內容會顯示在這裡 */}
<Outlet />
</main>
</div>
);
}
Data Loader (v6.4+)
React Router v6.4 引入了新的 Data API,允許你在路由定義中直接載入資料,雖然這需要改用 createBrowserRouter 的寫法,但能大幅提升解決「載入瀑布流」(Waterfalls) 的問題。
// router.js
import { createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Home />,
},
{
path: '/products/:id',
element: <ProductDetail />,
// 定義 loader,在渲染元件前先載入資料
loader: async ({ params }) => {
return fetch(`/api/products/${params.id}`);
},
},
]);
// ProductDetail.jsx
import { useLoaderData } from 'react-router-dom';
function ProductDetail() {
const product = useLoaderData(); // 取得 loader 回傳的資料
// ...
}
資料寫入 (Actions & Form)
與 loader 對應,action 用於處理寫入操作(如表單提交)。搭配 <Form> 元件,React Router 會自動處理請求的生命週期,並在操作完成後自動重新驗證 (revalidate) 資料,更新 UI。
// router.js
const router = createBrowserRouter([
{
path: '/login',
element: <Login />,
action: async ({ request }) => {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
// 呼叫後端 API
await fakeAuthProvider.signin(updates.email);
// 成功後重定向
return redirect('/');
},
},
]);
// Login.jsx
import { Form, useActionData } from 'react-router-dom';
function Login() {
const error = useActionData(); // 取得 action 回傳的錯誤資訊 (如果有)
return (
<Form method="post">
<input name="email" placeholder="Email" />
<button type="submit">登入</button>
{error && <p>{error.message}</p>}
</Form>
);
}
錯誤處理 (Error Handling)
React Router 內建了錯誤邊界 (Error Boundary) 的處理機制。當 loader、action 或元件渲染過程中發生錯誤時,會渲染 errorElement。
const router = createBrowserRouter([
{
path: '/',
element: <Home />,
errorElement: <ErrorPage />, // 定義錯誤頁面
},
]);
在錯誤頁面中,可以使用 useRouteError 來取得具體的錯誤資訊。
// ErrorPage.jsx
import { useRouteError } from 'react-router-dom';
function ErrorPage() {
const error = useRouteError();
console.error(error);
return (
<div>
<h1>糟糕!發生錯誤了。</h1>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}
私有路由 (Protected Routes)
在需要登入才能訪問的頁面,我們通常會建立一個包裹元件 (Wrapper Component) 來檢查使用者權限。
// PrivateRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './auth'; // 假設你有一個 Context 管理 Auth
function PrivateRoute({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
// 尚未登入,導向登入頁,並紀錄原本想去的頁面 (state.from)
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// App.jsx usage
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>;
其他常用 Hooks
useLocation
這個 Hook 會回傳目前的 location 物件,包含了 pathname、search、hash 以及 state 等資訊。這在需要根據當前路徑做判斷,或是讀取上一頁傳遞過來的 state 時非常有用。
import { useLocation } from 'react-router-dom';
function Page() {
const location = useLocation();
useEffect(() => {
// 追蹤頁面瀏覽 (Google Analytics)
ga.send(['pageview', location.pathname]);
}, [location]);
return <div>目前路徑: {location.pathname}</div>;
}