React Router v6:不使用localStorage实现登出后保留上一路由
描述
正在开发基于React Router v6和Apollo Client的React项目,需求是用户登出后保留其最后访问的路由,且不能使用localStorage。当前用户登出后,上一路由会被重置为默认值"/",而非保留最后访问的路由。
核心问题
登出过程中previousRouteRef会被重置为默认值"/",需要让它持久保留最后访问的路由。
预期行为
previousRouteRef.current能存储用户最后访问的路由,且在登出后保持不变,以便用户重新登录时可自动重定向到登出前的页面。
观察结果
登出时RoutesWrapper组件会重渲染三次,推测重复渲染导致previousRouteRef被重置为默认路由。
渲染计数验证
添加了渲染计数器跟踪RoutesWrapper的渲染次数,并在每次渲染时打印previousRouteRef的值,日志确认重渲染后previousRouteRef.current被重置为"/"。
涉及文件
RoutesWrapper.js
该组件负责路由管理与登录状态检查,包含用于存储最后访问路由的previousRouteRef:
import { useEffect, useRef, useState } from "react"; import LineWobbleLoader from "@/components/customs/helpers/page-loader"; import useAuthRedirect from "@/hooks/use-auth-redirect"; import MainLayout from "@/layout/main-layout"; import PageLayout from "@/layout/page-layout"; import ForgotPassword from "@/pages/auth-form/login/forgot-password-page"; import { Login } from "@/pages/auth-form/login/login"; import ResetPassword from "@/pages/auth-form/login/reset-password"; import { SignUp } from "@/pages/auth-form/signup/signup"; import SignUpVerify from "@/pages/auth-form/signup/signup-verify"; import AddContact from "@/pages/contacts/add-contact"; import Contacts from "@/pages/contacts/contacts"; import Dashboard from "@/pages/dashboard/dashboard"; import MessageTemplate from "@/pages/messages/message-templates"; import SetupPage from "@/pages/setup/setup-page"; import SuccessPage from "@/pages/success/success-page"; import CreateTemplate from "@/pages/templates/add-template"; import { Navigate, Route, Routes } from "react-router-dom"; function RoutesWrapper() { const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false); const previousRouteRef = useRef<string>("/"); const renderCount = useRef(0); const { meData, whatsappData, meLoading, whatsappLoading, refetchMe } = useAuthRedirect({ isLoggedIn }); renderCount.current += 1; console.log(`RoutesWrapper render count: ${renderCount.current}`); useEffect(() => { refetchMe(); }, [isLoggedIn, refetchMe]); if (meLoading || whatsappLoading) { return ( <div className="h-screen flex justify-center items-center"> <LineWobbleLoader /> </div> ); } return ( <Routes> {!meData ? ( <> <Route path="/" element={<Login setIsLoggedIn={setIsLoggedIn} previousRouteRef={previousRouteRef} />} /> <Route path="/signup" element={<SignUp />} /> <Route path="/forgot-password" element={<ForgotPassword />} /> <Route path="/reset-password" element={<ResetPassword />} /> <Route path="/success-page" element={<SuccessPage />} /> </> ) : ( <> <Route path="/" element={whatsappData?.getWhatsApp?.business ? <Navigate to="/contacts" replace /> : <Navigate to="/setup" replace />} /> <Route path="/setup" element={<SetupPage setIsLoggedIn={setIsLoggedIn} previousRouteRef={previousRouteRef} />} /> <Route element={<PageLayout />}> {whatsappData?.getWhatsApp?.business ? ( <Route element={<MainLayout previousRouteRef={previousRouteRef} meData={meData.me} setIsLoggedIn={setIsLoggedIn} />}> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/contacts" element={<Contacts />} /> <Route path="/addcontact" element={<AddContact />} /> <Route path="/templates" element={<CreateTemplate />} /> <Route path="/newtemplate" element={<MessageTemplate />} /> <Route path="*" element={<Navigate to="/" replace />} /> </Route> ) : ( <Route path="*" element={<Navigate to="/setup" replace />} /> )} </Route> </> )} </Routes> ); } export default RoutesWrapper;
useLogout.js
处理登出流程的Hook,本应在登出前将previousRouteRef设置为当前路径名:
import { useMutation } from "@apollo/client"; import { LOGOUT } from "@/services/gpl/mutations"; const useLogout = ({ setIsLoggedIn, previousRouteRef }) => { const [logout] = useMutation(LOGOUT); const handleLogout = () => { previousRouteRef.current = window.location.pathname; // Persisting the current route logout() .then(() => { setIsLoggedIn(false); window.location.href = "/"; }) .catch(error => { console.error("Logout error:", error); }); }; return { handleLogout }; }; export default useLogout;
useLogin.tsx
处理登录流程的Hook,登录后会根据previousRouteRef重定向:
import { useLazyQuery, useApolloClient } from "@apollo/client"; import { LOGIN, ME } from "@/services/gpl/queries"; import { useToast } from "@/components/ui/use-toast"; import { useNavigate } from "react-router-dom"; interface LoginInput { email: string; password: string; } type Props = { setIsLoggedIn: React.Dispatch<React.SetStateAction<boolean>>; previousRouteRef: React.MutableRefObject<string>; }; const useLogin = ({ setIsLoggedIn, previousRouteRef }: Props) => { const { toast } = useToast(); const navigate = useNavigate(); const client = useApolloClient(); const [login, { loading, error, data }] = useLazyQuery(LOGIN); const handleLogin = (loginInput: LoginInput) => { login({ variables: { login: loginInput } }) .then((response) => { if (response.data?.login) { setIsLoggedIn(true); const cachedMeData = client.readQuery({ query: ME }); if (cachedMeData && cachedMeData.me) { if (cachedMeData.me.email === response.data.login.email) { navigate(previousRouteRef.current || "/contacts"); return; } } navigate("/contacts"); } else { toast({ title: "Failed", description: error?.message, className: "text-red-600 border-red-600", }); } }) .catch((error) => { console.log(error); toast({ description: "Invalid email or password", className: "text-red-600 border-red-600", }); }); }; return { loading, error, data, handleLogin, }; }; export default useLogin;
已尝试方案
- 使用
useRef定义previousRouteRef存储登出前的最后路由,但登出后值被重置为"/"; - 在
handleLogout中手动设置previousRouteRef.current = window.location.pathname,但仍会被重置; - 调试渲染次数,确认登出时组件多次重渲染可能影响状态。
解决方案
问题根源
调用window.location.href = "/"会导致页面全量刷新,此时整个React应用重新初始化,组件内的useRef会被重置为初始值"/",这是问题的核心。
修复方案
改用URL查询参数传递登出前的路由,避免页面刷新,具体步骤:
- 修改
useLogout.js,替换页面刷新为React Router跳转并携带路由参数:
import { useMutation } from "@apollo/client"; import { LOGOUT } from "@/services/gpl/mutations"; import { useNavigate } from "react-router-dom"; const useLogout = ({ setIsLoggedIn, previousRouteRef }) => { const [logout] = useMutation(LOGOUT); const navigate = useNavigate(); const handleLogout = () => { const currentPath = window.location.pathname; // 排除登录相关页面,不需要保存这些路由 if (!["/", "/signup", "/forgot-password", "/reset-password"].includes(currentPath)) { previousRouteRef.current = currentPath; } logout() .then(() => { setIsLoggedIn(false); // 携带路由参数跳转,不刷新页面 navigate(`/?redirect=${encodeURIComponent(previousRouteRef.current)}`); }) .catch(error => { console.error("Logout error:", error); }); }; return { handleLogout }; }; export default useLogout;
- 修改
RoutesWrapper.js,读取URL参数更新previousRouteRef:
import { useEffect, useRef, useState } from "react"; import { Navigate, Route, Routes, useLocation } from "react-router-dom"; // 其他导入不变 function RoutesWrapper() { const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false); const previousRouteRef = useRef<string>("/"); const renderCount = useRef(0); const location = useLocation(); const { meData, whatsappData, meLoading, whatsappLoading, refetchMe } = useAuthRedirect({ isLoggedIn }); // 读取URL中的redirect参数,更新ref useEffect(() => { const searchParams = new URLSearchParams(location.search); const redirectPath = searchParams.get("redirect"); if (redirectPath) { previousRouteRef.current = decodeURIComponent(redirectPath); } }, [location.search]); renderCount.current += 1; console.log(`RoutesWrapper render count: ${renderCount.current}`); useEffect(() => { refetchMe(); }, [isLoggedIn, refetchMe]); // 已登录时自动跳转到保存的路由,同时清除URL参数 return ( <Routes> {!meData ? ( <> <Route path="/" element={<Login setIsLoggedIn={setIsLoggedIn} previousRouteRef={previousRouteRef} />} /> <Route path="/signup" element={<SignUp />} /> <Route path="/forgot-password" element={<ForgotPassword />} /> <Route path="/reset-password" element={<ResetPassword />} /> <Route path="/success-page" element={<SuccessPage />} /> </> ) : ( <> <Route path="/" element={ whatsappData?.getWhatsApp?.business ? <Navigate to={previousRouteRef.current || "/contacts"} replace /> : <Navigate to="/setup" replace /> } /> <Route path="/setup" element={<SetupPage setIsLoggedIn={setIsLoggedIn} previousRouteRef={previousRouteRef} />} /> <Route element={<PageLayout />}> {whatsappData?.getWhatsApp?.business ? ( <Route element={<MainLayout previousRouteRef={previousRouteRef} meData={meData.me} setIsLoggedIn={setIsLoggedIn} />}> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/contacts" element={<Contacts />} /> <Route path="/addcontact" element={<AddContact />} /> <Route path="/templates" element={<CreateTemplate />} /> <Route path="/newtemplate" element={<MessageTemplate />} /> <Route path="*" element={<Navigate to="/" replace />} /> </Route> ) : ( <Route path="*" element={<Navigate to="/setup" replace />} /> )} </Route> </> )} </Routes> ); } export default RoutesWrapper;
效果说明
修改后登出时不会刷新页面,通过React Router跳转并携带路由参数,保证previousRouteRef的值不会被重置,同时满足不使用localStorage的要求,用户登录后会自动重定向到登出前的页面。
内容的提问来源于stack exchange,提问作者iddi Msangi

