こんにちは、かつコーチです。
前回は、JWTを使ってログイン状態を保持する仕組みについて解説しました。
トークンを発行してフロント側で保持できるようになったら、次に考えたいのが「ログインしていない人には見せたくないページ」の扱いです。
マイページや管理画面のURLを直接入力されても、ログインしていなければ問答無用で弾く。
この仕組みを、Reactの世界ではProtected Route(保護されたルート)と呼びます。
今回は、React Routerと組み合わせてProtected Routeを実装する方法を、実際につまずいたポイントも交えて解説していきます。
Protected Routeとは?
未ログインユーザーを弾く仕組みの必要性
Webアプリの中には、ログイン済みのユーザーだけがアクセスできるページがたくさんあります。
マイページ、注文履歴、管理者用ダッシュボードなどが代表例です。
これらのページは、ボタンやリンクを非表示にするだけでは不十分です。
URLさえ知っていれば、ログインしていない状態でも直接アクセスできてしまうからです。
そこで、ページ単位で「ログインしているか」をチェックし、していなければログイン画面へ強制的に飛ばす仕組みが必要になります。
これがProtected Routeの役割です。
React Routerとの組み合わせ
Reactにはルーティング機能が標準搭載されていないため、多くのプロジェクトでReact Routerを使ってページ遷移を管理します。
Protected Routeは、このReact Routerが提供する<Navigate>コンポーネントや<Outlet>を活用して実装するのが一般的です。
「認証チェック用のラッパーコンポーネントを1つ作り、保護したいルートをそのラッパーで包む」というのが基本の考え方です。
実装手順
認証状態を判定するフック
まず、ログイン状態を判定するためのカスタムフックを用意します。
前回の記事で保存したJWTトークンの有無を使って、シンプルに判定してみましょう。
// useAuth.ts
import { useState, useEffect } from "react";
type AuthState = {
isAuthenticated: boolean;
isLoading: boolean;
};
export function useAuth(): AuthState {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem("accessToken");
setIsAuthenticated(!!token);
setIsLoading(false);
}, []);
return { isAuthenticated, isLoading };
}
ポイントは、isLoadingという状態も一緒に管理していることです。
理由は後ほどのつまずきポイントで説明します。
ProtectedRouteコンポーネントの実装
次に、認証チェックを行うラッパーコンポーネントを作ります。
<Outlet>を使うと、子ルートをまとめて保護できて便利です。
// ProtectedRoute.tsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "./useAuth";
export function ProtectedRoute() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <p>読み込み中...</p>;
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
ログインしていなければ/loginへリダイレクトし、ログイン済みなら<Outlet>で本来のページを表示します。
ルーティング側での組み込み
最後に、保護したいルートを<ProtectedRoute>で包みます。
// App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { ProtectedRoute } from "./ProtectedRoute";
import MyPage from "./pages/MyPage";
import Login from "./pages/Login";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute />}>
<Route path="/mypage" element={<MyPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
/mypageにアクセスがあったとき、まずProtectedRouteが実行され、認証済みの場合だけMyPageが描画される流れになります。
よくあるつまずきポイント
初期ロード中にリダイレクトしてしまう問題
私が実際に一度やってしまった失敗が、isLoadingの状態管理を省略したことによるバグです。
❌ Before:読み込み中の状態を考慮しない
export function ProtectedRoute() {
const token = localStorage.getItem("accessToken");
if (!token) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
一見問題なさそうに見えますが、非同期でユーザー情報をAPIから取得して認証状態を確定させるような構成にすると、初回描画時にはまだtokenの判定が完了していないケースが出てきます。
その結果、本来はログイン済みのはずのユーザーまで、一瞬ログインページに飛ばされてしまうという現象が起きました。
画面がチカチカと切り替わり、原因が分からず数十分悩んだ記憶があります。
✅ After:ロード中は判定を保留する
export function ProtectedRoute() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <p>読み込み中...</p>;
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
isLoadingがtrueの間はリダイレクト判定を行わず、認証チェックが完了してから初めて分岐させることで、この誤リダイレクトを防げます。
認証状態のような非同期処理を伴う判定では、「まだ分からない」という状態を明示的に扱うことが重要だと痛感した出来事でした。
応用・一歩先の使い方
ロールベースの制御(管理者専用ページなど)
実務では「ログインしていれば誰でもOK」ではなく、「管理者だけ」「有料会員だけ」といった、より細かい制御が必要になることもあります。
その場合は、許可するロールを引数として渡せるように拡張します。
// RoleProtectedRoute.tsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "./useAuth";
type Props = {
allowedRoles: string[];
};
export function RoleProtectedRoute({ allowedRoles }: Props) {
const { isAuthenticated, isLoading, role } = useAuth();
if (isLoading) return <p>読み込み中...</p>;
if (!isAuthenticated) return <Navigate to="/login" replace />;
if (!allowedRoles.includes(role)) return <Navigate to="/403" replace />;
return <Outlet />;
}
useAuth側で、JWTのペイロードからロール情報を取り出して返すように拡張しておくと、この判定に使えます。
リダイレクト後に元のページへ戻す
もう一つよくある要望が、「ログイン後に、もともと見ようとしていたページに戻したい」というものです。
<Navigate>のstateにリダイレクト前のパスを持たせておくことで実現できます。
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location.pathname }} replace />;
}
ログイン画面側で、このstate.fromを受け取ってログイン成功後の遷移先に使えば、ユーザー体験を損なわずに認証フローを組み込めます。
まとめ
この記事のポイント
- Protected Routeは、未ログインユーザーを特定ページから弾くための仕組み
React Routerの<Navigate>と<Outlet>を組み合わせて実装する- 認証状態の判定が非同期の場合は、
isLoading状態を必ず考慮する - ロールベースの制御やリダイレクト後の遷移先保持など、応用の幅も広い
次に読むべき記事
Protected Routeでページを守れるようになったら、次はもう一つの重要なセキュリティ観点、XSS(クロスサイトスクリプティング)対策について見ていきましょう。
→ 次の記事:ReactアプリのXSS対策