《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 /> 是 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(),和登录页保持一致。 这是全篇唯一一个需要你主动改的写法。


七、一张图看完整流程

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" />,先停一秒,问自己:登录页知道用户从哪来吗?

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

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


相关推荐
网络毒刘2 小时前
Token 用量观测实战:从会话结构拆前缀/工具/生成,建立个人降本仪表盘
人工智能·性能优化·token·cursor
记得开心一点嘛7 小时前
Trellora:基于 Electron、React 和本地知识库的 AI 知识工作台
人工智能·react.js·electron
500847 小时前
React Native for OpenHarmony 实战:三方库 react-native-url-polyfill 的鸿蒙化适配指南
javascript·react native·react.js·性能优化·electron·harmonyos
HLAIA光子9 小时前
RAG Chunks 切分优化后成本骤降 86%
后端·性能优化·架构
传奇开心果编程9 小时前
【现代声明式UI学与练】第9课 性能优化——渲染优化、列表优化、内存优化、启动优化
学习·flutter·react native·ui·性能优化·swiftui·android jetpack
5008412 小时前
React Native for OpenHarmony 实战:三方库 react-native-volume-control 的鸿蒙化适配指南
javascript·react native·react.js·electron·harmonyos
薛一半12 小时前
React-Redux三重优化实战揭秘
javascript·vue.js·react.js
Alice-YUE1 天前
RAF 渲染帧速通:从渲染帧到性能优化
性能优化
无名猿1 天前
移动构造与移动赋值:把资源偷过来
c++·性能优化·内存管理·现代c++·语法基础
yunwei371 天前
eBPF 开发实践:使用 eBPF 隐藏进程或文件信息
linux·后端·性能优化