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)

在 SPA 中,我們不使用 HTML 的 <a> 標籤來跳轉頁面,因為那會導致瀏覽器重新整理整個頁面。取而代之的是使用 <Link> 元件。

import { Link } from 'react-router-dom';

function Navbar() {
  return (
    <nav>
      <Link to="/">首頁</Link>
      <Link to="/about">關於我們</Link>
    </nav>
  );
}

<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) 的處理機制。當 loaderaction 或元件渲染過程中發生錯誤時,會渲染 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 物件,包含了 pathnamesearchhash 以及 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>;
}