15.1 什麼是 React Router?

React Router 是 React 的路由函式庫,讓單頁應用(SPA)可以有多個「頁面」。

npm install react-router-dom

15.2 基本路由設定

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

// ============================================================
// 頁面元件
// ============================================================
function HomePage() {
  return <h1>首頁</h1>;
}

function AboutPage() {
  return <h1>關於我們</h1>;
}

function ContactPage() {
  return <h1>聯絡我們</h1>;
}

function NotFoundPage() {
  return (
    <div style={{ textAlign: 'center', padding: '60px' }}>
      <h1 style={{ fontSize: '6rem', margin: 0 }}>404</h1>
      <p>找不到頁面</p>
      <Link to="/">回到首頁</Link>
    </div>
  );
}

// ============================================================
// 導航列
// ============================================================
function Navbar() {
  return (
    <nav style={{
      display: 'flex',
      gap: '16px',
      padding: '16px 24px',
      backgroundColor: '#2d3748',
    }}>
      <Link to="/" style={{ color: 'white', textDecoration: 'none' }}>首頁</Link>
      <Link to="/about" style={{ color: 'white', textDecoration: 'none' }}>關於</Link>
      <Link to="/contact" style={{ color: 'white', textDecoration: 'none' }}>聯絡</Link>
    </nav>
  );
}

// ============================================================
// App
// ============================================================
function App() {
  return (
    <BrowserRouter>
      <Navbar />
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/contact" element={<ContactPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
import { NavLink } from 'react-router-dom';

function Navbar() {
  const linkStyle = ({ isActive }) => ({
    color: isActive ? '#63b3ed' : 'white',
    textDecoration: 'none',
    fontWeight: isActive ? 'bold' : 'normal',
    borderBottom: isActive ? '2px solid #63b3ed' : 'none',
    padding: '8px 0',
  });

  return (
    <nav style={{ display: 'flex', gap: '20px', padding: '16px 24px', backgroundColor: '#2d3748' }}>
      <NavLink to="/" style={linkStyle} end>首頁</NavLink>
      <NavLink to="/about" style={linkStyle}>關於</NavLink>
      <NavLink to="/contact" style={linkStyle}>聯絡</NavLink>
    </nav>
  );
}

15.3 動態路由與路由參數

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

// 文章列表
function PostList() {
  const posts = [
    { id: 1, title: 'React 入門' },
    { id: 2, title: 'Hooks 完全指南' },
    { id: 3, title: 'React Router 教學' },
  ];

  return (
    <div>
      <h1>文章列表</h1>
      {posts.map((post) => (
        <div key={post.id}>
          <Link to={`/posts/${post.id}`}>{post.title}</Link>
        </div>
      ))}
    </div>
  );
}

// 文章詳情
function PostDetail() {
  const { postId } = useParams(); // 取得 URL 參數

  return (
    <div>
      <h1>文章 #{postId}</h1>
      <Link to="/posts">返回列表</Link>
    </div>
  );
}

// 路由設定
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/posts" element={<PostList />} />
        <Route path="/posts/:postId" element={<PostDetail />} />
      </Routes>
    </BrowserRouter>
  );
}

15.4 巢狀路由 (Nested Routes)

import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom';

// ============================================================
// Dashboard 佈局
// ============================================================
function DashboardLayout() {
  return (
    <div style={{ display: 'flex', minHeight: '100vh' }}>
      {/* 側邊欄 */}
      <nav style={{
        width: '200px',
        backgroundColor: '#2d3748',
        padding: '20px',
      }}>
        <h2 style={{ color: 'white' }}>Dashboard</h2>
        <ul style={{ listStyle: 'none', padding: 0 }}>
          <li><Link to="/dashboard" style={{ color: '#a0aec0' }}>總覽</Link></li>
          <li><Link to="/dashboard/users" style={{ color: '#a0aec0' }}>使用者</Link></li>
          <li><Link to="/dashboard/settings" style={{ color: '#a0aec0' }}>設定</Link></li>
        </ul>
      </nav>

      {/* 內容區(子路由會渲染在 Outlet) */}
      <main style={{ flex: 1, padding: '24px' }}>
        <Outlet />
      </main>
    </div>
  );
}

function DashboardHome() {
  return <h1>儀表板總覽</h1>;
}

function DashboardUsers() {
  return <h1>使用者管理</h1>;
}

function DashboardSettings() {
  return <h1>系統設定</h1>;
}

// ============================================================
// 路由設定
// ============================================================
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />

        {/* 巢狀路由 */}
        <Route path="/dashboard" element={<DashboardLayout />}>
          <Route index element={<DashboardHome />} />
          <Route path="users" element={<DashboardUsers />} />
          <Route path="settings" element={<DashboardSettings />} />
        </Route>

        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  );
}

15.5 程式化導航 (Programmatic Navigation)

import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';

function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();

  const handleLogin = () => {
    // 登入邏輯...

    // 導航到之前嘗試訪問的頁面,或預設到首頁
    const from = location.state?.from || '/';
    navigate(from, { replace: true });
  };

  return (
    <div>
      <h1>登入</h1>
      <button onClick={handleLogin}>登入</button>
      <button onClick={() => navigate(-1)}>返回上一頁</button>
    </div>
  );
}

// 查詢參數
function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get('q') || '';
  const page = Number(searchParams.get('page')) || 1;

  const handleSearch = (newQuery) => {
    setSearchParams({ q: newQuery, page: 1 });
  };

  const handlePageChange = (newPage) => {
    setSearchParams({ q: query, page: newPage });
  };

  return (
    <div>
      <input
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="搜尋..."
      />
      <p>搜尋:{query},第 {page} 頁</p>
      <button onClick={() => handlePageChange(page - 1)} disabled={page <= 1}>上一頁</button>
      <button onClick={() => handlePageChange(page + 1)}>下一頁</button>
    </div>
  );
}

15.6 路由保護 (Protected Routes)

import { Navigate, useLocation } from 'react-router-dom';

function ProtectedRoute({ children }) {
  const isAuthenticated = false; // 替換為實際的認證檢查
  const location = useLocation();

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location.pathname }} replace />;
  }

  return children;
}

// 使用
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/login" element={<LoginPage />} />

        {/* 受保護的路由 */}
        <Route
          path="/dashboard"
          element={
            <ProtectedRoute>
              <DashboardLayout />
            </ProtectedRoute>
          }
        >
          <Route index element={<DashboardHome />} />
          <Route path="settings" element={<DashboardSettings />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

15.7 練習題

練習 1:建立一個部落格應用

  • 首頁顯示文章列表
  • 點擊文章進入詳情頁 /posts/:id
  • 包含分類頁面 /categories/:category
  • 有 404 頁面

練習 2:建立一個管理後台

  • 登入頁面
  • 受保護的 Dashboard 路由
  • 巢狀路由(總覽、使用者管理、設定)
  • 麵包屑導航

本課重點回顧

  1. BrowserRouter + Routes + Route 定義路由
  2. Link / NavLink 用於導航
  3. useParams 取得動態路由參數
  4. Outlet 渲染巢狀路由的子元件
  5. useNavigate 程式化導航
  6. useSearchParams 管理查詢參數
  7. Navigate 元件實現路由保護