《React Router 受保护路由的 3 个坑,第 2 个 90% 的人都踩过》

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 />ProtectRoutechildren,只有校验通过才会被渲染。校验不过,渲染的直接是 <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(),和登录页保持一致。 这是全篇唯一一个需要你主动改的写法。


七、一张图看完整流程

sequenceDiagram participant U as &#34;用户&#34; participant P as &#34;ProtectRoute守卫&#34; participant L as &#34;登录页Login&#34; U->>P: 直接访问 /pay P->>P: 查 localStorage 发现未登录 P->>L: Navigate 到 /login 并带上 state.from=/pay L->>L: useLocation 读出 state.from U->>L: 填账号密码提交 L->>L: 登录成功 写入 isLogin L->>U: navigate(from, replace) 跳回 /pay

整条链路最关键的不是"拦",而是 state.from 这一棒交接------它在跳转时把"用户原本想去哪"悄悄传给了登录页。


结尾:记住一件事

路由守卫真正的难点从来不是"拦住",而是"拦完送回原处"。

下次你写 <Navigate to="/login" />,先停一秒,问自己:登录页知道用户从哪来吗?

一个行为改变:以后写受保护路由,默认把 useLocationstate.from 一起写上,别等用户投诉"回不了原页面"才补。

开放问题:你们项目的路由守卫,是放在路由配置层(像本文这样包一层组件),还是封装成高阶组件 / Hook?两种方案你更偏好哪种?评论区聊聊。


相关推荐
__zRainy__1 小时前
React开始:直接在网站中使用React.js
前端·react.js·前端框架
用户938515635073 小时前
React Router 进阶:路由守卫、登录鉴权与状态传递
前端·javascript·全栈
触底反弹4 小时前
🚀 20 行代码手写前端路由 + React Router 核心 API 一篇搞定!
前端·javascript·react.js
小林ixn5 小时前
React Router 从入门到实战:一篇搞定路由配置、懒加载与嵌套路由
前端·react.js·前端框架
张龙6875 小时前
10 万条数据不卡顿:不定高虚拟列表从原理到生产实现
前端·javascript·性能优化
先吃饱再说6 小时前
从多页到单页:前端路由的演进与 React Router
前端·react.js·前端框架
echoVic6 小时前
把 Orca 的 DeepSeek 缓存命中率打到 99%,我做了九轮优化
性能优化·ai编程·deepseek
仙人球部落 揞殺7 小时前
Sql Server查询性能优化之走出索引的误区
数据库·性能优化
生戎马 平安京策9 小时前
只学一点点:我的技术学习策略
前端·javascript·react.js