Skip to content

React Router v6 私有路由实现

React Router 版本提示

本文针对 React Router v6 版本,如果您使用的是 v5 或更早版本,实现方式会有所不同。

问题描述

在使用 React Router v6 时,开发者尝试创建私有路由组件来保护需要认证的页面,但遇到了以下错误:

Error: [PrivateRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>

原始代码示例:

jsx
// PrivateRoute.js
import React from 'react';
import { Route, Navigate } from "react-router-dom";
import { isauth } from 'auth'

function PrivateRoute({ element, path }) {
  const authed = isauth() // 根据 localStorage 返回 true 或 false
  const ele = authed === true ? element : <Navigate to="/Home" />;
  return <Route path={path} element={ele} />;
}

export default PrivateRoute
jsx
// route.js
<PrivateRoute exact path="/" element={<Dashboard/>}/>
<Route exact path="/home" element={<Home/>}/>

问题根源

在 React Router v6 中,<Routes> 组件只能直接包含 <Route><React.Fragment> 作为子元素。自定义组件(如 PrivateRoute)不能直接作为 <Routes> 的子元素使用。

解决方案

方案一:使用包装组件模式(推荐)

这是 React Router v6 官方推荐的认证模式:

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

const PrivateRoute = ({ children }) => {
  const isAuthenticated = isauth(); // 您的认证逻辑
  const location = useLocation();

  if (!isAuthenticated) {
    // 重定向到登录页,同时保存试图访问的地址
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
};

export default PrivateRoute;

使用方式:

jsx
// App.js
<Routes>
  <Route 
    path="/dashboard" 
    element={
      <PrivateRoute>
        <Dashboard />
      </PrivateRoute>
    } 
  />
  <Route path="/login" element={<Login />} />
  <Route path="/home" element={<Home />} />
</Routes>

方案二:使用 Outlet 组件

对于需要在布局中嵌套路由的情况,可以使用 <Outlet>

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

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

export default PrivateRouteWithOutlet;

使用方式:

jsx
// App.js
<Routes>
  <Route element={<PrivateRouteWithOutlet />}>
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/profile" element={<Profile />} />
  </Route>
  <Route path="/login" element={<Login />} />
</Routes>

方案三:函数式组件调用

通过函数调用而非 JSX 组件的方式:

jsx
// PrivateRoute.js
const PrivateRoute = ({ element, path }) => {
  const authed = isauth();
  const ele = authed ? element : <Navigate to="/Home" />;
  return <Route path={path} element={ele} />;
};

export default PrivateRoute;

使用方式:

jsx
// route.js
<Route element={<MainLayout />}>
  <Route index element={<HomePage />} />
  <Route path="about" element={<AboutPage />} />
  {PrivateRoute({ element: <SecurePage />, path: 'secure' })}
</Route>

方案四:集中式路由配置

对于大型应用,可以使用集中式路由管理:

jsx
// routes.js
export const publicRoutes = [
  {
    path: '/',
    element: <Home />,
  },
  {
    path: '/login',
    element: <Login />,
  }
];

export const privateRoutes = [
  {
    path: '/dashboard',
    element: <Dashboard />,
  },
  {
    path: '/profile',
    element: <Profile />,
  }
];
jsx
// AppRoutes.js
import { useRoutes } from 'react-router-dom';
import { publicRoutes, privateRoutes } from './routes';
import PrivateRoute from './PrivateRoute';

export function AppRoutes() {
  const parseRouteObjects = (routes, isPrivate = false) => {
    return routes.map(route => ({
      path: route.path,
      element: isPrivate ? 
        <PrivateRoute>{route.element}</PrivateRoute> : 
        route.element,
    }));
  };

  const publicRouteObjects = parseRouteObjects(publicRoutes);
  const privateRouteObjects = parseRouteObjects(privateRoutes, true);
  
  const routes = useRoutes([...publicRouteObjects, ...privateRouteObjects]);
  
  return routes;
}

最佳实践建议

  1. 认证状态管理:使用 React Context 或状态管理库(如 Redux)来管理全局认证状态
  2. 路由保护:保护路由时考虑用户角色和权限
  3. 用户体验:在重定向时保存原始目标地址,登录后能够返回原页面
  4. 加载状态:在处理认证时显示适当的加载指示器
jsx
// 使用 Context 管理认证状态
const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // 初始化认证状态检查
    checkAuthStatus().then(user => {
      setUser(user);
      setLoading(false);
    });
  }, []);
  
  if (loading) {
    return <div>加载中...</div>;
  }
  
  return (
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
};

常见问题解答

Q: 为什么不能像 v5 那样使用自定义路由组件?

A: React Router v6 引入了新的声明式 API,要求路由结构更加明确和严格,这提高了性能并减少了歧义。

Q: 如何处理嵌套的私有路由?

A: 使用 <Outlet> 组件可以轻松实现嵌套路由的认证保护,父路由的认证状态会自动传递给所有子路由。

Q: 如何实现基于角色的访问控制?

A: 可以扩展 PrivateRoute 组件,增加角色验证逻辑:

jsx
const RoleProtectedRoute = ({ children, requiredRole }) => {
  const { user } = useAuth();
  
  if (!user) {
    return <Navigate to="/login" />;
  }
  
  if (user.role !== requiredRole) {
    return <Navigate to="/unauthorized" />;
  }
  
  return children;
};

总结

React Router v6 通过强制使用标准 <Route> 组件作为 <Routes> 的直接子元素,提供了更加一致和高效的路由处理机制。虽然这改变了 v5 中创建自定义路由组件的方式,但新的模式提供了更好的性能和更清晰的代码结构。

使用包装组件模式或 <Outlet> 组件是实现私有路由的推荐方法,它们与 v6 的设计理念保持一致,同时提供了灵活性和强大的功能。

升级提示

如果您从 React Router v5 升级到 v6,请参阅官方升级指南以获取更多迁移建议。