我不知道的 React-Router:Link、NavLink 与 useNavigate 导航解析

571 字 10 min read
React React-Router Link NavLink useNavigate SPA History API

在单页应用 (SPA) 中,流畅的页面切换体验至关重要。React Router 提供了几种关键工具来实现这一目标,其中最常用的就是 Link 组件、NavLink 组件以及 useNavigate Hook。

Link 组件:客户端导航的基础

Link 组件是 React Router 中实现声明式导航的核心。它看起来像一个普通的 <a> 标签,但行为却大不相同。

Link 与 <a> 标签的核心区别

  • <a> 标签 (原生 HTML):\n * 点击 <a> 标签会触发浏览器向服务器发送一个新的 HTTP 请求,请求 href 指定的 URL。\n * 服务器响应后,浏览器会完全重新加载整个页面,即使目标页面和当前页面共享很多资源。\n * 这会导致页面闪烁、状态丢失,破坏了 SPA 的流畅体验。\n * 示例: <a href="/about">关于我们</a>\n
  • <Link> 组件 (React Router):\n * Link 组件渲染出来也是一个 <a> 标签,但它添加了额外的逻辑。\n * 阻止默认行为: 它会监听点击事件,并调用 event.preventDefault() 来阻止浏览器执行默认的页面跳转和刷新。\n * 更新 URL: 接着,它会使用 History API (通常是 history.pushState()) 来在浏览器历史记录中添加新的条目更新地址栏的 URL,但不会向服务器发送请求。\n * 触发 React 更新: URL 的变化会被 React Router 监听(通常由 BrowserRouterHashRouter 实现),进而通知相关的 <Route> 组件根据新的 URL 渲染对应的视图内容。React 会高效地更新 DOM,只改变需要变化的部分。\n * 这样就实现了在不刷新整个页面的情况下切换视图,保持了应用的单页特性。\n * 示例: <Link to="/about">关于我们</Link>\n

Link 的内部工作流程 (简化版)

  1. 用户点击 <Link to="/target"> 组件渲染出的 <a> 标签。
  2. Link 组件内部的 onClick 处理函数被触发。
  3. 处理函数调用 event.preventDefault()
  4. 处理函数获取 to prop 指定的目标路径 (/target)。
  5. 处理函数调用 React Router 的 history 对象的方法(如 history.push('/target'))。
  6. history.push 内部调用浏览器的 window.history.pushState({}, '', '/target') 来更新 URL 和浏览器历史记录。
  7. React Router 的路由监听器(如 BrowserRouter 内部的 popstate 监听器或 history 库的 listen 方法)检测到 URL 变化。
  8. React Router 根据新的 URL 匹配对应的 <Route>
  9. 匹配到的 <Route> 及其关联的组件被渲染,React 更新 DOM。

NavLink 组件:添加激活状态样式

NavLinkLink 的一个特殊版本,它知道当前链接是否"激活"(即其 to prop 是否匹配当前的 URL),并允许你方便地为激活状态添加样式。

如何判断激活状态?

NavLink 默认在以下情况被认为是激活的:

  • 当前 URL 完全匹配 to prop 的路径
  • 如果设置了 end prop (boolean),则只有在完全匹配时才激活(忽略后续的 / 或子路径)。默认 end={false},表示 /usersNavLink/users/123 路径下也会被视为激活(部分匹配)。

添加激活样式的方式

  1. activeClassName (v5 及更早版本,v6 中已废弃): 传递一个 CSS 类名,当链接激活时会自动添加到 <a> 标签上。
  2. activeStyle (v5 及更早版本,v6 中已废弃): 传递一个内联样式对象,当链接激活时会自动应用。
  3. 函数作为 classNamestyle prop (v6+ 推荐): 这是 v6 中最灵活的方式。你可以给 classNamestyle prop 传递一个函数,该函数接收一个包含 isActive 布尔值的对象作为参数,你可以根据 isActive 的值动态返回类名或样式。
import { NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      {/* 使用函数 className */}
      <NavLink
        to="/messages"
        className={({ isActive }) =>
          isActive ? 'active-link' : 'inactive-link'
        }
      >
        消息
      </NavLink>

      {/* 使用函数 style */}
      <NavLink
        to="/tasks"
        style={({ isActive }) => ({
          color: isActive ? 'red' : 'black',
          fontWeight: isActive ? 'bold' : 'normal'
        })}
      >
        任务
      </NavLink>

      {/* 使用 end prop 精确匹配 */}
      <NavLink to="/home" end className={({isActive}) => isActive ? 'active' : ''}>
        首页
      </NavLink>
    </nav>
  );
}

useNavigate Hook:编程式导航

LinkNavLink 适用于用户点击触发的导航。但有时我们需要在代码逻辑中(例如,表单提交成功后、用户执行某个操作后)进行页面跳转,这时就需要编程式导航

useNavigate Hook 提供了执行编程式导航的能力。

如何使用?

  1. 在你的函数组件中调用 useNavigate() Hook,它会返回一个 navigate 函数。
  2. 调用 navigate(to, options) 函数来执行跳转。
import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();

  const handleSubmit = async (event) => {
    event.preventDefault();
    // 假设 loginUser 是一个异步登录函数
    const loginSuccess = await loginUser(/* ... */);

    if (loginSuccess) {
      // 登录成功后跳转到仪表盘
      navigate('/dashboard');
    } else {
      // 显示错误信息...
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* ...表单元素... */}
      <button type="submit">登录</button>
    </form>
  );
}

navigate 函数的参数

  • to (string): 目标路径,与 Linkto prop 类似。
  • to (number): 可以传入一个数字,如 navigate(-1) 来模拟浏览器的"后退"按钮,navigate(1) 模拟"前进",navigate(-2) 后退两步等。
  • options (object): 可选配置对象:
    • replace: true: 如果设置为 true,则导航会替换历史记录中的当前条目,而不是添加新条目。这意味着用户点击浏览器后退按钮时,不会回到执行 navigate 之前的页面。常用于登录成功后的跳转,避免用户回退到登录页。
    • state: any: 允许你将一些状态数据附加到新的历史记录条目上。这个状态数据不会显示在 URL 中,但可以在目标路由组件中通过 useLocation().state 访问。适用于传递临时数据,如"来自哪个页面"的信息,或者一些需要在跳转后恢复的 UI 状态。
function ProductDetail({ product }) {
  const navigate = useNavigate();

  const handleAddToCart = () => {
    // ... 添加到购物车的逻辑 ...

    // 跳转到购物车页面,并替换当前历史记录,
    // 同时传递一个状态表明是从哪个产品添加的
    navigate('/cart', {
      replace: true,
      state: { addedProductId: product.id, message: '成功添加到购物车!' }
    });
  };

  return <button onClick={handleAddToCart}>添加到购物车</button>;
}

// 在 /cart 页面对应的组件中:
import { useLocation } from 'react-router-dom';

function CartPage() {
  const location = useLocation();
  const message = location.state?.message; // 读取 navigate 传递过来的状态

  return (
    <div>
      {message && <p style={{color: 'green'}}>{message}</p>}
      {/* ... 购物车内容 ... */}
    </div>
  );
}

Link 与 state Prop

Link 组件也支持 state prop,功能与 navigatestate 选项完全相同,用于在声明式导航中传递路由状态。

<Link to="/profile" state={{ from: '/settings' }}>
  前往个人资料
</Link>

// 在 /profile 页面组件中
function ProfilePage() {
  const location = useLocation();
  const fromPage = location.state?.from; // 获取 'settings'
  // ...
}

总结

  • Link: 用于用户点击触发的基础客户端导航,避免页面刷新,通过 to 指定目标。
  • NavLink: Link 的增强版,能感知激活状态,并通过 classNamestyle 的函数 prop 应用激活样式。
  • useNavigate: 用于在 JS 逻辑中执行编程式导航,支持 replacestate 选项,也能实现历史记录的前进后退。

理解这三者的区别和用法,是构建流畅、交互自然的 React SPA 的关键。它们共同利用了 History API 和 React 的渲染机制,提供了比传统多页应用更好的用户体验。