React Router v6私有路由中,未渲染的条件子组件useEffect为何仍会执行?
我在使用React Router v6时,通过PrivateRoute组件包裹需要登录才能访问的页面组件。但遇到一个问题:即使用户未认证,被包裹的子组件中的useEffect仍然会执行,导致不必要的数据请求(虽然不影响功能,但希望避免)。
我的PrivateRoute代码如下:
import React, { useEffect, useState } from 'react' import { Navigate, useNavigate } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import { logOutUser } from './store/userSlice' import NSF from './views/NSF' import { refreshToken } from './api/userApi' import { tokenCurrent } from './utils/userUtils' const PrivateRoute = ({ component: Component, props, role = 'Meet Administrator', ...rest }) => { const navigate = useNavigate() const dispatch = useDispatch() const user = useSelector((state) => state.user) const [isAuthenticated, setIsAuthenticated] = useState(null) const [isNSF, setIsNSF] = useState(true) useEffect(() => { if (!tokenCurrent()) { setIsAuthenticated(false) dispatch(logOutUser()) navigate('/login') } else { if ( user.roles && (user.roles.includes('WebAdmin') || user.roles.includes(role)) ) { setIsNSF(false) } setIsAuthenticated(true) } refreshToken() // eslint-disable-next-line }, [user, dispatch, Component]) if (isAuthenticated === null) { return <> </> } if (isNSF && isAuthenticated) { return <NSF /> } return !isAuthenticated ? ( <Navigate to='/login' /> ) : ( <Component {...props} {...rest} /> ) } export default PrivateRoute
路由使用示例:
<Routes> <Route path='/parents' element={<PrivateRoute component={ParentsPage} />} /> </Routes>
我封装PrivateRoute的核心目的是避免在每个需要认证的组件中重复检查登录状态,但现在子组件的副作用会提前执行,希望解决这个问题。
你当前的写法存在一个关键问题:当你在路由中写<PrivateRoute component={ParentsPage} />时,ParentsPage已经被提前实例化了。也就是说,不管PrivateRoute最终是否决定渲染它,ParentsPage的组件实例已经创建,它的useEffect等生命周期钩子自然会执行。
React Router v6的路由机制是通过element prop传递组件实例,原来v5风格的PrivateRoute写法(接收component或render prop)不再适配v6的逻辑,才会导致这种提前实例化的问题。
我们需要调整PrivateRoute的实现,确保只有在认证通过后才会渲染目标组件,避免提前实例化。这里提供两种常用的方案:
方案1:将PrivateRoute改为子组件包装器(推荐)
修改PrivateRoute,让它接收children而不是component prop,这样只有在认证通过时才会渲染子组件:
import React, { useEffect, useState } from 'react' import { Navigate, useNavigate } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import { logOutUser } from './store/userSlice' import NSF from './views/NSF' import { refreshToken } from './api/userApi' import { tokenCurrent } from './utils/userUtils' const PrivateRoute = ({ children, role = 'Meet Administrator' }) => { const navigate = useNavigate() const dispatch = useDispatch() const user = useSelector((state) => state.user) const [isAuthenticated, setIsAuthenticated] = useState(null) const [isNSF, setIsNSF] = useState(true) useEffect(() => { const checkAuth = async () => { if (!tokenCurrent()) { setIsAuthenticated(false) dispatch(logOutUser()) navigate('/login') } else { // 异步处理refreshToken,避免状态更新混乱 try { await refreshToken() // 根据用户角色判断是否显示NSF组件 if (user.roles && (user.roles.includes('WebAdmin') || user.roles.includes(role))) { setIsNSF(false) } setIsAuthenticated(true) } catch (err) { // 刷新token失败,跳转登录页 setIsAuthenticated(false) dispatch(logOutUser()) navigate('/login') } } } checkAuth() }, [user, dispatch, navigate, role]) if (isAuthenticated === null) { return <></> // 可替换为自定义加载动画 } if (isNSF && isAuthenticated) { return <NSF /> } return isAuthenticated ? children : <Navigate to='/login' replace /> } export default PrivateRoute
路由使用方式调整为:
<Routes> <Route path='/parents' element={ <PrivateRoute> <ParentsPage /> </PrivateRoute> } /> {/* 指定角色的受保护路由 */} <Route path='/admin' element={ <PrivateRoute role='WebAdmin'> <AdminPage /> </PrivateRoute> } /> </Routes>
方案2:使用Outlet实现嵌套路由组
如果你的项目有多个受保护页面,可以把PrivateRoute作为父路由,内部用Outlet渲染子路由,让所有子路由自动继承认证逻辑:
// 修改后的PrivateRoute const PrivateRoute = ({ role = 'Meet Administrator' }) => { const navigate = useNavigate() const dispatch = useDispatch() const user = useSelector((state) => state.user) const [isAuthenticated, setIsAuthenticated] = useState(null) const [isNSF, setIsNSF] = useState(true) useEffect(() => { const checkAuth = async () => { if (!tokenCurrent()) { setIsAuthenticated(false) dispatch(logOutUser()) navigate('/login') } else { try { await refreshToken() if (user.roles && (user.roles.includes('WebAdmin') || user.roles.includes(role))) { setIsNSF(false) } setIsAuthenticated(true) } catch (err) { setIsAuthenticated(false) dispatch(logOutUser()) navigate('/login') } } } checkAuth() }, [user, dispatch, navigate, role]) if (isAuthenticated === null) { return <></> } if (isNSF && isAuthenticated) { return <NSF /> } return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace /> }
路由使用方式:
<Routes> {/* 公共路由 */} <Route path='/login' element={<LoginPage />} /> {/* 普通权限的受保护路由组 */} <Route element={<PrivateRoute />}> <Route path='/parents' element={<ParentsPage />} /> <Route path='/dashboard' element={<DashboardPage />} /> </Route> {/* 管理员权限的受保护路由组 */} <Route element={<PrivateRoute role='WebAdmin' />}> <Route path='/admin' element={<AdminPage />} /> <Route path='/settings' element={<SettingsPage />} /> </Route> </Routes>
通过上述修改,目标组件(比如ParentsPage)只会在PrivateRoute确认用户认证通过后才会被渲染,此时组件才会被实例化,它的useEffect等副作用钩子才会执行,从而彻底避免了未认证时的不必要请求。
另外,我把refreshToken改成异步处理,是因为原来的代码中如果refreshToken是异步函数,可能会导致状态更新顺序混乱,调整后逻辑会更稳定。
内容的提问来源于stack exchange,提问作者saschoff

