从“迷路”到“瞬移”:彻底搞懂 React Router 的 `useLocation` 与 `useNavigate`

5 阅读6分钟

在构建单页应用(SPA)时,前端路由是核心骨架。我们习惯了用 <Link> 跳转页面,用 <Route> 定义路径。但在复杂的业务场景中,仅仅会配置路由是远远不够的。

你是否遇到过这样的场景:

  • 用户未登录时点击“个人中心”,跳转登录页,登录成功后却只能回到首页,而不是刚才想去的“个人中心”?
  • 表单提交后,用户刷新页面导致数据重复提交?
  • 在复杂的嵌套路由中,搞不清当前的 URL 参数到底在哪里?

解决这些问题的关键,就在于深入理解 React Router 提供的两个核心 Hooks:useLocation 和 useNavigate。它们不仅是跳转和获取参数的工具,更是掌控用户浏览历史、实现丝滑鉴权体验的“核武器”。

本文将结合实战代码,带你彻底吃透这两个 API,从原理到最佳实践,让你的路由控制能力上一个台阶。


📍 一、 useLocation:不仅仅是获取 URL 参数

很多开发者对 useLocation 的理解仅停留在“获取查询参数(Query Params)”上。实际上,它是当前激活路由的状态快照

1. location 对象的全貌

当你在组件中调用 const location = useLocation() 时,你拿到的是一个包含以下属性的对象:

// 假设当前 URL 为: http://127.0.0.1:5500/#/login?redirect=%2Fuser%2F123

{
  pathname: "/login",                // 路由路径(不含查询参数)
  search: "?redirect=%2Fuser%2F123", // 查询字符串
  hash: "",                          // hash 值(在 HashRouter 中通常为空,因为路径已被解析)
  state: null,                       // 关键!这是通过 navigate 传递的“隐式”数据
  key: "xs63g7"                      // 唯一标识,用于跟踪历史记录条目
}

2. 实战:解析 URL 参数

最基础的用法是解析 search 字段。虽然我们可以手写正则,但现代浏览器原生提供了 URLSearchParams,结合 useLocation 使用非常优雅:

import { useLocation } from 'react-router-dom';

const SearchPage = () => {
  const { search } = useLocation();
  
  // 自动处理 ? 符号,解析为键值对
  const params = new URLSearchParams(search);
  const query = params.get('q'); // 获取 ?q=xxx 中的值

  return <div>搜索关键词:{query}</div>;
};

🚀 二、 useNavigate:掌控跳转的“方向盘”

useNavigate 取代了 v5 版本的 history.push,它更加灵活,支持命令式导航。

1. 基础跳转与参数传递

import { useNavigate } from 'react-router-dom';

const MyComponent = () => {
  const navigate = useNavigate();

  const goToDetail = () => {
    // 1. 字符串跳转
    navigate('/detail/123');
    
    // 2. 数字跳转(类似 history.go)
    // navigate(-1); // 后退一页
  };

  return <button onClick={goToDetail}>查看详情</button>;
};

2. 核心秘籍:replace 模式

这是很多新手容易忽略,但在实际开发中至关重要的配置。

场景:  用户登录。
如果用户访问 /pay(支付页),被拦截跳转到 /login。登录成功后,如果使用默认的 navigate('/pay'),浏览器的历史记录栈会变成:[/pay, /login, /pay]
此时,用户在支付页点击“后退”,会回到登录页!这显然是不合理的。

解决方案:  使用 replace: true
它会用新页面替换当前页面在历史记录中的位置。

// Login.jsx
const handleLogin = () => {
  // 模拟登录成功
  localStorage.setItem('isLogin', 'true');
  
  // 第二个参数配置 replace: true
  // 这会将 /login 的记录替换为 /pay,栈变为 [/pay, /pay] -> 实际上看起来像直接跳转
  navigate('/pay', { replace: true });
};

对比图解:

表格

模式历史栈变化 (假设原栈:[A])用户点击“后退”的行为
默认 (Push)[A, B, C]C -> B -> A
Replace[A, C] (B被C替换)C -> A

🛡️ 三、 终极实战:实现“登录后重定向回原页面”

这是前端鉴权中最经典的需求。用户未登录访问 /user/123,跳转登录,登录后自动回到 /user/123

这需要 useLocation 的 state 和 useNavigate 的配合。

1. 第一步:受保护路由 (ProtectedRoute)

我们需要一个高阶组件或包装组件,在渲染子组件前检查登录状态。如果未登录,跳转至登录页,并携带当前路径信息

// ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';

const ProtectedRoute = ({ children }) => {
  const isLogin = localStorage.getItem('isLogin') === 'true';
  const location = useLocation(); // 获取当前试图访问的位置

  if (!isLogin) {
    // 未登录,重定向到登录页
    // 关键点:通过 state 传递 from 信息
    return (
      <Navigate 
        to="/login" 
        state={{ from: location }} // 我从哪里来告诉登录页
        replace // 替换掉当前的访问记录避免死循环
      />
    );
  }

  return children;
};

export default ProtectedRoute;

2. 第二步:登录页 (Login)

登录页需要读取 state,知道用户原本想去哪。如果 state 为空(比如用户直接访问登录页),则默认跳回首页。

// Login.jsx
import { useNavigate, useLocation } from 'react-router-dom';

const Login = () => {
  const navigate = useNavigate();
  const location = useLocation();
  
  // 核心逻辑:获取来源路径,如果没有则默认为 '/'
  // location.state?.from?.pathname 获取原本想去的地址
  const from = location.state?.from?.pathname || '/';

  const handleSubmit = (e) => {
    e.preventDefault();
    // ... 模拟登录请求 ...
    
    // 登录成功
    localStorage.setItem('isLogin', 'true');
    
    // 跳转回来源页面,并使用 replace 清除登录页的历史记录
    navigate(from, { replace: true });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h1>请登录</h1>
      {/* 表单内容 */}
      <button type="submit">登录</button>
    </form>
  );
};

export default Login;

3. 第三步:路由配置

在 App 组件中,将需要鉴权的页面包裹在 ProtectedRoute 中。

// App.jsx
import { Routes, Route } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
import Pay from './pages/Pay';
import Login from './pages/Login';

const App = () => {
  return (
    <Routes>
      <Route path="/login" element={<Login />} />
      
      {/* 需要鉴权的路由 */}
      <Route 
        path="/pay" 
        element={
          <ProtectedRoute>
            <Pay />
          </ProtectedRoute>
        } 
      />
    </Routes>
  );
};

🧠 四、 深度解析:为什么 state 比 search 参数更好?

你可能会问: “我能不能把来源路径拼在 URL 后面,比如 /login?redirect=/pay?”

答案是可以,但使用 location.state 有以下优势:

  1. URL 整洁state 是隐式传递的,不会污染地址栏,用户刷新页面时(如果是编程式导航),参数依然存在于 history state 中(但在强刷时会丢失,需注意)。
  2. 类型安全state 可以传递对象、布尔值等复杂数据,而 search 只能是字符串。
  3. 语义清晰state 专用于页面间的数据传递,search 专用于资源过滤和排序。

注意:  location.state 的数据在用户强制刷新页面(F5)后会丢失。因此,对于极其重要的数据(如支付订单号),建议还是通过 URL 参数或后端接口获取,但对于“从哪来回哪去”这种导航辅助信息,state 是最佳选择。


📌 五、 总结与最佳实践

  1. 鉴权跳转必用 replace:无论是从 ProtectedRoute 跳往 Login,还是 Login 跳回原页面,都建议使用 { replace: true },避免用户点击后退时陷入“登录页死循环”。
  2. 利用 state 传递上下文:不要把 useLocation 仅仅当作获取 ?id=1 的工具,利用 state 在页面间传递来源信息,能极大提升用户体验。
  3. 默认回退路径:在处理 location.state?.from 时,永远要准备一个兜底路径(通常是 / 或 /dashboard),防止用户直接访问登录页导致跳转错误。
  4. 区分 HashRouter 与 BrowserRouter:虽然 API 一致,但在 HashRouter 中,location.pathname 不包含 # 后的部分,且 location.hash 通常为空(被 React Router 内部消费了),这点在打印日志时要注意。

掌握 useLocation 和 useNavigate,你就掌握了 React 应用的“导航系统”。希望这篇文章能帮你写出更健壮、体验更流畅的路由逻辑!