如果用户已登录且试图访问登录页面,如何将其重定向到首页?
我有一个组件,在用户通过以下方式登录后,会将他们重定向到他们试图访问的受保护路由:
export const ProtectedRoute = () => {
const { token } = useAuth()
const location = useLocation()
if (!token) {
return <Navigate to="/login" replace state={{ from: location }} />
}
return <Outlet />
}
和
setToken(token)
const origin = location.state?.from?.pathname || '/'
navigate(origin)
在登录时。这个功能单独使用时就能正常工作。
然而,我想新增一个功能:如果用户在登录后已经完成身份验证而试图访问登录页,则将他们重定向到首页,方式如下:
export const RestrictedPublicRoute = () => {
const { token } = useAuth()
if (token) {
return <Navigate to="/" replace />
}
return <Outlet />
}
他们似乎在导航方面有点矛盾,因为之前的路由保持已经不再保持,因为它们在登录后都被路由到 "/",因为第二次导航在用户登录并且有令牌的瞬间覆盖了第一次导航。如何构造一个情况,使得第二个导航仅在你在已经登录的情况下尝试访问登录页时才运行?
这些是我的路由配置:
<Route element={<ProtectedRoute />}>
<Route path="/" element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="admin" element={<Admin />} />
</Route>
<Route element={<RestrictedPublicRoute />}>
<Route path="login" element={<Login />} />
</Route>
下面是一个展示问题的CodePen链接: https://codesandbox.io/p/live/a60b37c5-251e-4373-b387-ad7b354c8da4
解决方案
问题
这属于React与 React-Router的经典问题之一:排队的状态更新是异步处理的(即更新身份验证状态),而导航操作是立即生效的(即将用户重定向回他正在访问的受保护路由)。
- 用户成功完成身份验证,并且
- 身份验证状态被排队处理
- 用户按预期重定向到
"/admin" - 受保护的路由
ProtectedRoute发现token尚未更新,于是重定向回"/login" - 现在
token状态已经更新,React重新渲染,RestrictedPublicRoute看到更新后的token状态并且现在重定向到"/"
解决方案建议
通常可以通过在身份验证流程中引入加载状态来解决,视你在初始身份验证状态何时/如何加载而定,路由守卫也可能需要加载状态(你使用localStorage,因此在初始渲染周期就能获取到,因此目前这样是可行的)。
我建议使用React 19的 transitions,将身份验证流程包裹在一个已经包含加载/待定状态的转换中。
src/utils/AuthProvider.jsx:
- 从React导入
useTransition - 调用
useTransition钩子并解构加载标志和 "startTransition" 回调 - 在身份验证上下文值中返回加载状态
- 在登录处理程序中将身份验证逻辑包装在React转换中
import {
useCallback, // Note 1
useMemo, // Note 1
useState,
useTransition, // <-- (1)
} from "react";
import { useNavigate, useLocation } from "react-router";
import { AuthContext } from "./AuthContext";
...
const AuthProvider = ({ children }) => {
const location = useLocation();
const navigate = useNavigate();
const [isLoading, startTransition] = useTransition(); // <-- (2)
const [token, setToken] = useState(() => localStorage.getItem("token")); // Note 2
const handleLogin = useCallback(async (event) => { // Note 1
event.preventDefault();
startTransition(async () => { // <-- (4)
const token = await fakeAuth();
localStorage.setItem("token", token);
setToken(token);
const origin = location.state?.from?.pathname || "/";
navigate(origin, { replace: true });
})
}, [location, navigate]);
const handleLogout = useCallback(() => { // Note 1
setToken(null); // Note 2
localStorage.removeItem("token");
navigate("/login", { replace: true });
}, [navigate]);
const value = useMemo(() => ({ // Note 1
token,
onLogin: handleLogin,
onLogout: handleLogout,
isLoading // <-- (3)
}), [isLoading, handleLogin, handleLogout, token]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
1 你还需要养成提供稳定上下文值的习惯。使用 useMemo 钩子对提供的上下文值进行记忆化。使用 useMemo/useCallback/ 等为上下文属性提供稳定的引用。如果你还没有,可能会考虑将 eslint-plugin-react-hooks,或等效工具,集成到你的项目中以帮助实现这一点。
2 当某个值尚未存储在localStorage时,将返回 null。你可能想在用户登出时,通过将 token 重新设回 null 来维持一致的状态不变量。
src/Routes.jsx:
从身份验证上下文解构 isLoading 值,并在任何身份验证更新待处理时延迟导航动作,即当 isLoading 返回 true 时。
在这个示例中,它只是简单地等到 isLoading 为false才会检查令牌,但你可以设想一个更高级的逻辑,能够条件性地渲染加载UI等内容。
import { useAuth } from "./utils/AuthHook";
import { Navigate, useLocation, Outlet } from "react-router";
export const ProtectedRoute = () => {
const location = useLocation();
const { token, isLoading } = useAuth();
if (!isLoading && !token) {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <Outlet />;
};
export const RestrictedPublicRoute = () => {
const { token, isLoading } = useAuth();
if (!isLoading && token) {
return <Navigate to="/" replace />;
}
return <Outlet />;
};