React Router 路由守卫:为什么用户登录后总回不了原页面?
开篇:一个让用户懵掉的场景
你做了一个需要登录才能看的页面 /pay。用户兴冲冲点进去,啪------被弹到登录页。他输完账号密码登录成功,结果页面跳回了首页。
用户愣了:我明明是想看 /pay,怎么又回首页了?
这个 bug 我见过太多次。路由没坏,是你忘了告诉登录页"用户从哪来"。
这篇文章就是解决这个问题的。
一、什么是路由守卫(受保护路由)
路由守卫 = 在路由真正渲染目标页面之前,先检查"你有没有资格看",没资格就拦去登录页。
把它想成小区门禁:你想进 /pay 这栋楼,保安(ProtectRoute)先看你的门禁卡(localStorage 里的 isLogin)。没卡?保安把你请到门卫室(/login)登记,登记完再放你进原本要去的那栋楼。
关键不是"拦住",而是拦完之后还能送回原处------这正是大多数人漏掉的一环。
二、先写一个最朴素的守卫
骨架其实只有三步:读登录态 → 没登录就 <Navigate> 走人 → 登录了就正常渲染 children。
jsx
// protectRoute.jsx
import { Navigate } from 'react-router-dom';
const ProtectRoute = ({ children }) => {
// 🔑 用 localStorage 模拟登录态:取出来是字符串,必须 === 'true' 才认
const isLogin = localStorage.getItem('isLogin') === 'true';
if (!isLogin) {
// ⚠️ 这里如果只写 <Navigate to="/login" />,登录后就回不了原页面了
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
return <>{children}</>;
};
export default ProtectRoute;
在路由表里,把它"包"在受保护的页面外面:
jsx
// App.jsx 路由配置片段
<Route
path="/pay"
element={
// 🔑 ProtectRoute 自己不是页面,它是个"壳"
<ProtectRoute>
<Pay />
</ProtectRoute>
}
/>
<Pay />是ProtectRoute的children,只有校验通过才会被渲染。校验不过,渲染的直接是<Navigate>,页面瞬间跳走。
三、核心:怎么记住"用户从哪来"
这是整篇文章的重点,也是 90% 的人写错的地方。
❌ 常见的错误写法:只拦不记
jsx
// 只拦不记------用户登录后你根本不知道他原本要去哪
if (!isLogin) {
return <Navigate to="/login" />; // 没带任何来源信息
}
登录页登录成功后,通常只能写死 navigate('/'),于是用户永远回首页。
✅ 正确写法:用 location 的 state 把来源页"夹带"过去
jsx
// protectRoute.jsx(修正版,用 useLocation 更稳妥)
import { Navigate, useLocation } from 'react-router-dom';
const ProtectRoute = ({ children }) => {
const location = useLocation(); // 🔑 拿"当前路由"信息,而不是全局 window.location
const isLogin = localStorage.getItem('isLogin') === 'true';
if (!isLogin) {
// 🔑 state.from 把 /pay 藏在跳转里,一起带去登录页
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
return <>{children}</>;
};
区别在于:错误写法把"用户从哪来"弄丢了;正确写法在跳转时顺手塞了一个
state.from。登录页之后就能把它读出来。
四、登录页怎么读回这个来源
登录页用 useLocation() 把 state.from 取出来,登录成功后跳回原处,而不是跳首页。
jsx
// Login/index.jsx
import { useNavigate, useLocation } from 'react-router-dom';
const Login = () => {
const navigate = useNavigate();
const location = useLocation();
// 🔑 可选链:用户直接访问 /login 时 state 为空,回退到首页 '/'
const from = location.state?.from || '/';
function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const username = formData.get('username');
const password = formData.get('password');
if (username === 'admin' && password === '123456') {
localStorage.setItem('isLogin', 'true');
// 🔑 replace: true 让 /login 从历史记录里消失,回退时不会又回到登录页
navigate(from, { replace: true });
}
}
return (
<form onSubmit={handleSubmit}>
<h1>登录</h1>
<input name="username" placeholder="请输入用户名" />
<input name="password" placeholder="请输入密码" required />
<button type="submit">登录</button>
</form>
);
};
export default Login;
五、为什么一定要 replace
这是一个容易忽略、但体验差异巨大的细节。整条链路在浏览器历史记录里的变化是:
text
用户访问 /pay → 历史栈: [ /pay ]
守卫拦下来 → 历史栈: [ /login ] (replace 把 /pay 顶掉)
登录成功跳回 → 历史栈: [ /pay ] (replace 把 /login 顶掉)
两次 replace 之后,历史栈里干净地只剩 /pay。 用户按浏览器"后退",会回到进入 /pay 之前那个页面,而不是又掉回登录页。
少了 replace,用户登录完一按后退又看见登录页------自己都懵。
六、一个真实的坑:别用全局 location
回看第一版 protectRoute.jsx,它写的是 location.pathname,而不是 useLocation().pathname。
那个 location 是浏览器全局对象 window.location。碰巧也能跑------因为 ProtectRoute 只在 /pay 命中时才挂载,此刻 window.location.pathname 确实等于 /pay。
但它很脆:
-
它不是 React Router 的"当前路由",而是真实 URL。一旦守卫被复用、组件不随路由卸载,就可能读到错误的值;
-
团队的人读代码会困惑:"这个
location哪来的?"
结论:守卫里统一用 useLocation(),和登录页保持一致。 这是全篇唯一一个需要你主动改的写法。
七、一张图看完整流程
整条链路最关键的不是"拦",而是
state.from这一棒交接------它在跳转时把"用户原本想去哪"悄悄传给了登录页。
结尾:记住一件事
路由守卫真正的难点从来不是"拦住",而是"拦完送回原处"。
下次你写 <Navigate to="/login" />,先停一秒,问自己:登录页知道用户从哪来吗?
一个行为改变:以后写受保护路由,默认把 useLocation 和 state.from 一起写上,别等用户投诉"回不了原页面"才补。
开放问题:你们项目的路由守卫,是放在路由配置层(像本文这样包一层组件),还是封装成高阶组件 / Hook?两种方案你更偏好哪种?评论区聊聊。