【React Router 教學】#05 導航守衛與路由保護實戰

測驗:React Router 導航守衛與路由保護實戰

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

1. 什麼是路由守衛(Route Guard)的主要功能?

  • A. 加快頁面載入速度
  • B. 自動處理 API 請求錯誤
  • C. 在使用者進入頁面前檢查權限
  • D. 管理瀏覽器的歷史紀錄

2. Navigate 元件和 useNavigate Hook 的主要差異是什麼?

  • A. Navigate 只能用於登入頁面,useNavigate 可以用於任何頁面
  • B. Navigate 是元件,渲染時立即跳轉;useNavigate 是 Hook,呼叫時才跳轉
  • C. Navigate 會保留瀏覽紀錄,useNavigate 不會
  • D. Navigate 不能帶狀態,useNavigate 可以帶狀態

3. 在 <Navigate to="/login" replace /> 中,replace 屬性的作用是什麼?

  • A. 導向時不在瀏覽紀錄留下一筆,使用者按上一頁不會回來
  • B. 替換目前頁面的內容而不跳轉
  • C. 強制重新載入登入頁面
  • D. 清除所有瀏覽紀錄

4. 使用 useLocation Hook 可以取得哪些資訊?

  • A. 只有目前的路徑名稱(pathname)
  • B. 使用者的登入狀態和權限
  • C. 所有路由的設定資訊
  • D. 目前路徑、查詢字串、錨點和傳遞的狀態

5. 在 PrivateRoute 元件中,為什麼要使用 state={{ from: location }} 傳遞資訊?

  • A. 為了在登入頁面顯示錯誤訊息
  • B. 為了記錄使用者的瀏覽歷史
  • C. 為了讓使用者登入後能返回原本想去的頁面
  • D. 為了驗證使用者的身份

一句話說明

在路由切換前檢查使用者是否有權限,沒有就導去登入頁。

前置知識

  • 第 4 篇:巢狀路由與 Outlet 佈局

什麼是路由守衛?

路由守衛(Route Guard)就是在使用者進入某個頁面之前,先檢查他有沒有權限。

使用者想去 /dashboard
        ↓
    有登入嗎?
    ↙      ↘
  有         沒有
   ↓          ↓
進入頁面    導去 /login

React Router 沒有內建路由守衛,但我們可以用元件組合的方式實作。


Navigate 元件:宣告式重導向

最小範例

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

function Dashboard() {
  const isLoggedIn = false;

  if (!isLoggedIn) {
    return <Navigate to="/login" />;  // 沒登入就導去 /login
  }

  return <h1>Dashboard</h1>;
}
Code language: JavaScript (javascript)

逐行翻譯

import { Navigate } from "react-router-dom";  // 引入重導向元件

if (!isLoggedIn) {
  return <Navigate to="/login" />;  // 如果沒登入,回傳 Navigate 元件
}
// ↑ Navigate 一被渲染就會自動跳轉到 to 指定的路徑
Code language: JavaScript (javascript)

常見變化

變化 1:加上 replace

<Navigate to="/login" replace />
Code language: HTML, XML (xml)

翻譯:導向 /login,但不會在瀏覽紀錄留下一筆(使用者按上一頁不會回來)

變化 2:帶狀態傳遞

<Navigate to="/login" state={{ from: location }} />
Code language: HTML, XML (xml)

翻譯:導向 /login,同時把「從哪裡來」的資訊帶過去


useNavigate Hook:程式化導航

最小範例

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

function LoginButton() {
  const navigate = useNavigate();

  function handleLogin() {
    // 登入成功後...
    navigate("/dashboard");  // 用程式導向 /dashboard
  }

  return <button onClick={handleLogin}>登入</button>;
}
Code language: JavaScript (javascript)

逐行翻譯

const navigate = useNavigate();  // 取得導航函式
navigate("/dashboard");          // 呼叫它就會跳轉
Code language: JavaScript (javascript)

Navigate vs useNavigate

Navigate useNavigate
是元件,寫在 JSX 裡 是 Hook,寫在函式裡
渲染時立即跳轉 呼叫時才跳轉
適合條件判斷 適合事件處理
// Navigate:條件渲染時用
if (!user) return <Navigate to="/login" />;

// useNavigate:按鈕點擊時用
onClick={() => navigate("/dashboard")}
Code language: JavaScript (javascript)

常見用法

const navigate = useNavigate();

// 基本導航
navigate("/users");

// 帶參數
navigate("/users/123");

// 返回上一頁
navigate(-1);

// 前進一頁
navigate(1);

// 導航並取代目前紀錄
navigate("/dashboard", { replace: true });

// 導航並帶狀態
navigate("/dashboard", { state: { from: "login" } });
Code language: JavaScript (javascript)

useLocation:取得當前路由資訊

最小範例

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

function CurrentPath() {
  const location = useLocation();

  return <p>目前在:{location.pathname}</p>;
}
Code language: JavaScript (javascript)

location 物件結構

const location = useLocation();

console.log(location);
// {
//   pathname: "/users/123",    // 目前路徑
//   search: "?sort=name",      // 查詢字串
//   hash: "#section1",         // 錨點
//   state: { from: "login" },  // 傳遞的狀態
//   key: "abc123"              // 唯一識別碼
// }
Code language: JavaScript (javascript)

一句話:location 是「你現在在哪裡」的完整資訊。


路由守衛元件設計模式

PrivateRoute 元件

這是最常見的路由保護模式:

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

function PrivateRoute({ children }) {
  const isLoggedIn = checkAuth();  // 檢查是否登入
  const location = useLocation();

  if (!isLoggedIn) {
    // 沒登入:導去 login,並記住原本要去哪
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  // 有登入:顯示原本的內容
  return children;
}
Code language: JavaScript (javascript)

逐行翻譯

function PrivateRoute({ children }) {     // children 是被包住的元件
  const isLoggedIn = checkAuth();         // 檢查登入狀態
  const location = useLocation();         // 取得目前位置

  if (!isLoggedIn) {
    return <Navigate
      to="/login"                         // 導向登入頁
      state={{ from: location }}          // 記住「從哪來」
      replace                             // 不留瀏覽紀錄
    />;
  }

  return children;                        // 通過檢查,顯示內容
}
Code language: PHP (php)

使用方式

// 方法 1:包住單一元件
<Route
  path="/dashboard"
  element={
    <PrivateRoute>
      <Dashboard />
    </PrivateRoute>
  }
/>

// 方法 2:配合 Outlet 保護整組路由
<Route element={<PrivateRoute><Outlet /></PrivateRoute>}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/settings" element={<Settings />} />
  <Route path="/profile" element={<Profile />} />
</Route>
Code language: PHP (php)

完整登入流程範例

1. 建立認證 Context

// AuthContext.jsx
import { createContext, useContext, useState } from "react";

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  function login(userData) {
    setUser(userData);
  }

  function logout() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}
Code language: JavaScript (javascript)

2. 建立 PrivateRoute 元件

// PrivateRoute.jsx
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "./AuthContext";

export function PrivateRoute({ children }) {
  const { user } = useAuth();
  const location = useLocation();

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

  return children;
}
Code language: JavaScript (javascript)

3. 建立登入頁面(含返回原頁面)

// LoginPage.jsx
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "./AuthContext";

export function LoginPage() {
  const [email, setEmail] = useState("");
  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();

  // 取得「從哪裡來」,沒有就預設去首頁
  const from = location.state?.from?.pathname || "/";

  function handleSubmit(e) {
    e.preventDefault();

    // 登入成功
    login({ email });

    // 導回原本要去的頁面
    navigate(from, { replace: true });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <button type="submit">登入</button>
    </form>
  );
}
Code language: JavaScript (javascript)

4. 設定路由

// App.jsx
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
import { AuthProvider } from "./AuthContext";
import { PrivateRoute } from "./PrivateRoute";
import { LoginPage } from "./LoginPage";
import { Dashboard } from "./Dashboard";
import { Settings } from "./Settings";

function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          {/* 公開路由 */}
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<LoginPage />} />

          {/* 受保護的路由 */}
          <Route element={<PrivateRoute><Outlet /></PrivateRoute>}>
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/settings" element={<Settings />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
}
Code language: JavaScript (javascript)

流程圖解

使用者點擊 /dashboard
        ↓
    PrivateRoute 檢查
        ↓
    user 存在嗎?
    ↙         ↘
  存在          不存在
   ↓              ↓
顯示 Dashboard   Navigate to /login
                 帶著 state: { from: "/dashboard" }
                        ↓
                 使用者在登入頁輸入帳號
                        ↓
                 登入成功
                        ↓
                 navigate(from) → 回到 /dashboard
Code language: JavaScript (javascript)

常見變化

變化 1:角色權限檢查

function RoleRoute({ children, allowedRoles }) {
  const { user } = useAuth();
  const location = useLocation();

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

  if (!allowedRoles.includes(user.role)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return children;
}

// 使用
<Route element={<RoleRoute allowedRoles={["admin"]}><Outlet /></RoleRoute>}>
  <Route path="/admin" element={<AdminPanel />} />
</Route>
Code language: PHP (php)

變化 2:Loading 狀態處理

function PrivateRoute({ children }) {
  const { user, loading } = useAuth();
  const location = useLocation();

  // 還在檢查登入狀態
  if (loading) {
    return <div>Loading...</div>;
  }

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

  return children;
}
Code language: JavaScript (javascript)

變化 3:登出後導向

function LogoutButton() {
  const { logout } = useAuth();
  const navigate = useNavigate();

  function handleLogout() {
    logout();
    navigate("/login", { replace: true });
  }

  return <button onClick={handleLogout}>登出</button>;
}
Code language: JavaScript (javascript)

Vibe Coder 檢查點

看到路由守衛相關代碼時確認:

  • [ ] PrivateRoute 有正確檢查登入狀態嗎?
  • [ ] Navigate 有加 replace 嗎?(避免使用者按上一頁繞過)
  • [ ] 有用 state 記住「從哪來」嗎?(登入後才能返回)
  • [ ] LoginPage 有處理 location.state?.from 嗎?
  • [ ] 有處理 loading 狀態嗎?(避免閃一下再跳轉)

核心概念翻譯表

你會看到 意思
<Navigate to="/login" /> 立即重導向到 /login
<Navigate replace /> 重導向但不留紀錄
state={{ from: location }} 把「從哪來」的資訊帶過去
const navigate = useNavigate() 取得程式化導航的函式
navigate(-1) 返回上一頁
navigate("/path", { replace: true }) 導向並取代目前紀錄
const location = useLocation() 取得目前的路由資訊
location.state?.from 取得傳過來的「從哪來」資訊
<PrivateRoute>{children}</PrivateRoute> 包住需要保護的內容

系列總結

恭喜你完成 React Router 系列!讓我們回顧一下:

篇章 學到的重點
#01 BrowserRouter、Routes、Route 基本設定
#02 Link、NavLink 導航,useParams 取得動態參數
#03 useSearchParams 處理查詢字串
#04 巢狀路由、Outlet 佈局
#05 Navigate、useNavigate、useLocation、路由守衛

現在你應該能看懂 AI 生成的 React Router 代碼了!


延伸:知道就好

這些進階功能遇到再查:

  • loader/action:React Router 6.4+ 的資料載入方式
  • defer/Await:延遲載入與 Suspense 整合
  • errorElement:路由層級的錯誤邊界
  • lazy:動態載入路由元件

進階測驗:React Router 導航守衛與路由保護實戰

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

1. 你正在開發一個電商網站,需要保護 /checkout 結帳頁面只讓已登入的使用者存取。同時,你希望未登入使用者被導向登入頁後,登入成功能自動返回結帳頁面。最佳做法是? 情境題

  • A. 在 Checkout 元件中使用 if (!user) return <Navigate to="/login" />
  • B. 在 Checkout 元件中使用 useNavigate 呼叫 navigate("/login")
  • C. 使用 PrivateRoute 包住 Checkout,並傳遞 state={{ from: location }} 到登入頁
  • D. 在路由設定中直接寫 <Route path="/checkout" element={user ? <Checkout /> : <Login />} />

2. 小明實作了一個 PrivateRoute,但使用者反映按瀏覽器的「上一頁」按鈕可以繞過登入檢查回到受保護頁面。問題出在哪裡? 錯誤診斷

function PrivateRoute({ children }) { const { user } = useAuth(); if (!user) { return <Navigate to=”/login” />; } return children; }
  • A. 沒有使用 useLocation 取得目前位置
  • B. Navigate 元件缺少 replace 屬性,導致重導向會留在瀏覽紀錄中
  • C. 應該使用 useNavigate 而不是 Navigate 元件
  • D. checkAuth 函式應該改用 useAuth Hook

3. 你的網站有管理員後台(/admin),需要檢查使用者是否登入且角色為 admin。你應該如何設計 RoleRoute 元件? 情境題

  • A. 只檢查 user 是否存在,不需要檢查角色
  • B. 先檢查角色,如果角色不符就導向登入頁
  • C. 在每個 admin 頁面的元件內部各自檢查角色
  • D. 先檢查登入狀態(導向 /login),再檢查角色權限(導向 /unauthorized)

4. 小華的登入頁面在登入成功後總是導向首頁,無法返回使用者原本想去的頁面。以下程式碼有什麼問題? 錯誤診斷

function LoginPage() { const { login } = useAuth(); const navigate = useNavigate(); function handleSubmit(e) { e.preventDefault(); login({ email }); navigate(“/”); // 登入後導向首頁 } return <form onSubmit={handleSubmit}>…</form>; }
  • A. 應該使用 Navigate 元件而不是 useNavigate
  • B. navigate 呼叫應該放在 login 之前
  • C. 沒有使用 useLocation 取得 state.from,應該導向 location.state?.from?.pathname || "/"
  • D. 需要加上 { replace: true } 選項

5. 你的應用程式在初次載入時,會先向後端 API 確認使用者的登入狀態。在等待 API 回應期間,PrivateRoute 會短暫將使用者重導向到登入頁面,造成畫面閃爍。如何解決這個問題? 情境題

  • A. 將 API 呼叫改為同步執行
  • B. 在 PrivateRoute 中加入 loading 狀態判斷,載入中時顯示 Loading 畫面而非重導向
  • C. 使用 setTimeout 延遲重導向
  • D. 將 user 的預設值設為 true

發佈留言

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