Skip to content

React Router v6 での PrivateRoute 実装

React Router v6 では、認証が必要なプライベートルートの実装方法が v5 から大きく変更されました。この記事では、よくあるエラー「Error: [PrivateRoute] is not a <Route> component」の解決策と、正しい実装方法を解説します。

問題の原因

React Router v6 では、<Routes> の直接の子要素は <Route> コンポーネントまたは <React.Fragment> のみ許可されます。v5 のようにカスタムの <PrivateRoute> コンポーネントを直接使用することはできません。

以下のような実装はエラーになります:

jsx
// 間違った実装例
<Routes>
  <PrivateRoute path="/dashboard" element={<Dashboard />} />
  <Route path="/home" element={<Home />} />
</Routes>

解決策

方法1: ラッパーコンポーネントを使う(推奨)

React Router v6 の公式ドキュメントで推奨されている方法です:

jsx
// ProtectedRoute.js
import { Navigate, useLocation } from 'react-router-dom';

export const ProtectedRoute = ({ children }) => {
  const isAuthenticated = isauth(); // 認証状態をチェック
  const location = useLocation();
  
  if (!isAuthenticated) {
    // ログインページにリダイレクトし、元の場所を保存
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  return children;
};

// App.js
import { Routes, Route } from 'react-router-dom';
import { ProtectedRoute } from './ProtectedRoute';

function App() {
  return (
    <Routes>
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        }
      />
      <Route path="/login" element={<Login />} />
      <Route path="/home" element={<Home />} />
    </Routes>
  );
}

方法2: Outlet を利用する

ネストされたルート構造に適した方法です:

jsx
// PrivateRoute.js
import { Navigate, Outlet } from 'react-router-dom';

const PrivateRoute = () => {
  const isAuthenticated = isauth();
  
  return isAuthenticated ? <Outlet /> : <Navigate to="/login" />;
};

// App.js
import { Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Routes>
      <Route element={<PrivateRoute />}>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
      </Route>
      <Route path="/login" element={<Login />} />
      <Route path="/home" element={<Home />} />
    </Routes>
  );
}

方法3: 高階コンポーネントパターン

コンポーネントを直接ラップする方法:

jsx
// private.js
import { Navigate } from 'react-router-dom';

export const privateRoute = (Component) => {
  const isAuthenticated = isauth();
  
  return isAuthenticated ? <Component /> : <Navigate to="/login" />;
};

// routes.js
import { privateRoute } from './private';

<Routes>
  <Route path="/dashboard" element={privateRoute(Dashboard)} />
  <Route path="/home" element={<Home />} />
</Routes>

ベストプラクティス

TIP

  1. 認証状態の管理: React Context または状態管理ライブラリを使用して認証状態を一元管理する
  2. リダイレクト時の状態保持: useLocation フックで元のパスを保存し、ログイン後に戻れるようにする
  3. ローディング状態: 非同期の認証チェック中はローディング表示を追加する

WARNING

Navigate コンポーネントは useEffect 内ではなく、直接レンダリング結果として返す必要があります。そうしないと、React のルールに違反する可能性があります。

完全な実装例

以下はコンテキストを使用した完全な実装例です:

jsx
import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export const useAuth = () => {
  return useContext(AuthContext);
};

export const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // 認証状態のチェック(ローカルストレージなどから)
    const user = localStorage.getItem('user');
    if (user) {
      setCurrentUser(JSON.parse(user));
    }
    setLoading(false);
  }, []);
  
  const value = {
    currentUser,
    login: (userData) => {
      localStorage.setItem('user', JSON.stringify(userData));
      setCurrentUser(userData);
    },
    logout: () => {
      localStorage.removeItem('user');
      setCurrentUser(null);
    }
  };
  
  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
};
jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';

export const ProtectedRoute = ({ children }) => {
  const { currentUser } = useAuth();
  const location = useLocation();
  
  if (!currentUser) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  return children;
};
jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider, useAuth } from './AuthContext';
import { ProtectedRoute } from './ProtectedRoute';

function AppRoutes() {
  return (
    <Routes>
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        }
      />
      <Route path="/login" element={<Login />} />
      <Route path="/" element={<Home />} />
    </Routes>
  );
}

function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <AppRoutes />
      </BrowserRouter>
    </AuthProvider>
  );
}

まとめ

React Router v6 では、プライベートルートの実装方法が変更され、より宣言的で柔軟なアプローチが可能になりました。ラッパーコンポーネントパターンを使用することで、クリーンで保守性の高いコードを実現できます。認証状態の管理には React Context を活用し、ユーザーエクスペリエンスを考慮したリダイレクト処理を実装しましょう。

詳細は React Router 公式ドキュメントを参照してください。