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

0 阅读4分钟

React Router 路由守卫:为什么用户登录后总回不了原页面?


开篇:一个让用户懵掉的场景

你做了一个需要登录才能看的页面 /pay。用户兴冲冲点进去,啪——被弹到登录页。他输完账号密码登录成功,结果页面跳回了首页

用户愣了:我明明是想看 /pay,怎么又回首页了?

这个 bug 我见过太多次。路由没坏,是你忘了告诉登录页"用户从哪来"

这篇文章就是解决这个问题的。


一、什么是路由守卫(受保护路由)

路由守卫 = 在路由真正渲染目标页面之前,先检查"你有没有资格看",没资格就拦去登录页。

把它想成小区门禁:你想进 /pay 这栋楼,保安(ProtectRoute)先看你的门禁卡(localStorage 里的 isLogin)。没卡?保安把你请到门卫室(/login)登记,登记完再放你进原本要去的那栋楼

关键不是"拦住",而是拦完之后还能送回原处——这正是大多数人漏掉的一环。


二、先写一个最朴素的守卫

骨架其实只有三步:读登录态 → 没登录就 <Navigate> 走人 → 登录了就正常渲染 children


// 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;

在路由表里,把它"包"在受保护的页面外面:


// App.jsx 路由配置片段

<Route

path="/pay"

element={

// 🔑 ProtectRoute 自己不是页面,它是个"壳"

<ProtectRoute>

<Pay />

</ProtectRoute>

}

/>

<Pay />ProtectRoutechildren,只有校验通过才会被渲染。校验不过,渲染的直接是 <Navigate>,页面瞬间跳走。


三、核心:怎么记住"用户从哪来"

这是整篇文章的重点,也是 90% 的人写错的地方。

❌ 常见的错误写法:只拦不记


// 只拦不记——用户登录后你根本不知道他原本要去哪

if (!isLogin) {

return <Navigate to="/login" />; // 没带任何来源信息

}

登录页登录成功后,通常只能写死 navigate('/'),于是用户永远回首页。

✅ 正确写法:用 location 的 state 把来源页"夹带"过去


// 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 取出来,登录成功后跳回原处,而不是跳首页。


// 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

这是一个容易忽略、但体验差异巨大的细节。整条链路在浏览器历史记录里的变化是:


用户访问 /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?两种方案你更偏好哪种?评论区聊聊。