陌上人如玉
公子世无双

6.5 编程式导航

6.5 编程式导航

一句话:用 useNavigate() 在「事件处理、异步回调、Effect」里跳转——它支持 replace(替换历史)、state(传状态)与相对路径(.. / .);渲染时的重定向用 <Navigate />。

一、基础用法

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

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

  const handleLogin = async () => {
    await login();
    navigate('/dashboard');                 // ⭐ 跳转到 /dashboard
  };

  return <button onClick={handleLogin}>登录</button>;
}
// 三种常用跳转形式
const navigate = useNavigate();

navigate('/about');                                     // ① 绝对路径
navigate('settings');                                   // ② 相对路径(当前路由下的 settings)
navigate(-1);                                           // ③ 数字(历史记录里的偏移)
navigate(-1);                                           //    后退一步
navigate(1);                                            //    前进一步
参数 含义
'/about' 绝对路径
'settings' 相对路径(相对于当前路由)
-1 / 1 历史记录偏移(后退 / 前进,等价 history.go(-1))

二、replace 与 state

// ① replace:替换当前历史记录(不会留下「后退」的一步)
navigate('/login', { replace: true });

// 典型场景:登录成功后跳转到首页
// ❌ 不加 replace → 用户点「后退」会回到登录页(体验不好)
navigate('/dashboard');                     // ⚠️ 后退会回到 /login
navigate('/dashboard', { replace: true });  // ✅ 后退不会回到登录页
// ② state:传递「不进 URL」的数据
navigate('/checkout', { state: { from: 'cart', items } });

// 接收方用 useLocation 读取
function Checkout() {
  const location = useLocation();
  const { from, items } = (location.state as { from?: string; items?: OrderItem[] }) ?? {};
  // ⚠️ state 不在 URL 里 → 刷新后仍然保留(存在 history 条目里),
  //    但「直接打开链接」时没有 state → 需要兜底处理
  return <div>来自 {from},共 {items?.length ?? 0} 件</div>;
}
state 的适用场景:
✅ 传递「临时/敏感/无需分享」的数据(如表单草稿、来源页面、列表滚动位置)
❌ 不适合传递「需要分享/刷新恢复」的数据(应该放 URL)
⚠️ 刷新页面时 state 仍在(因为它在 history 条目里),
   但如果用户「复制链接分享」或「直接打开」,state 就是 undefined → 必须有兜底

三、相对路径

// 假设当前路径是 /users/123/orders
const navigate = useNavigate();

navigate('detail');        // → /users/123/orders/detail(子路径)
navigate('..');            // → /users/123(上一级)
navigate('../..');         // → /users
navigate('.');             // → /users/123/orders(当前路径)
navigate('/about');        // → /about(绝对路径)
// ⚠️ 相对路径在「嵌套路由」里很有用(不用拼绝对路径)
function OrderActions() {
  const navigate = useNavigate();

  const goToDetail = () => navigate('detail');         // ⭐ 不用写 /users/123/orders/detail
  const goBackToList = () => navigate('..');

  return (
    <>
      <button onClick={goToDetail}>查看详情</button>
      <button onClick={goBackToList}>返回列表</button>
    </>
  );
}
// ✅ <Link> 也支持相对路径
<Link to="detail">详情</Link>
<Link to="..">返回</Link>
<Link to=".." relative="path">返回</Link>
// relative="route"(默认):相对「当前路由」
// relative="path":相对「当前路径」
// ⚠️ 两者在「匹配了通配符/多层路由」时有差异,通常默认即可

四、Navigate 组件 vs useNavigate

场景 用哪个
事件处理里跳转(点击、提交后) useNavigate()
异步回调里跳转(请求成功/失败后) useNavigate()
渲染时的条件重定向(未登录) <Navigate />
路由配置里的静态重定向 <Navigate />
// ✅ 渲染时重定向(声明式)
function RequireAuth({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return <>{children}</>;
}
// ⚠️ 不要在渲染中调用 navigate(会有副作用 + 可能循环)
function Bad() {
  const navigate = useNavigate();
  const { user } = useAuth();

  if (!user) {
    navigate('/login');                    // ❌ 渲染期间调用 navigate
  }
  return <div>...</div>;
}

// ✅ 用 <Navigate />(声明式)
function Good() {
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" replace />;
  return <div>...</div>;
}

// ✅ 或放到 Effect 里(如果必须用命令式)
useEffect(() => {
  if (!user) navigate('/login', { replace: true });
}, [user, navigate]);

五、常见场景

// ① 登录后跳回「来源页面」
function Login() {
  const navigate = useNavigate();
  const location = useLocation();
  const from = (location.state as { from?: Location })?.from?.pathname ?? '/dashboard';

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await login();
    navigate(from, { replace: true });     // ⭐ 回到来源,且不留登录页历史
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

// 配合 RequireAuth 的写法
function RequireAuth({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();
  const location = useLocation();
  if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
  return <>{children}</>;
}
// ② 提交成功后跳转(带提示)
const handleSubmit = async () => {
  await save();
  toast.success('保存成功');
  navigate('/list', { replace: true });
};

// ③ 删除后返回列表(或直接后退)
const handleDelete = async (id: string) => {
  await remove(id);
  navigate(-1);                            // ⭐ 后退一步(回到列表)
  // 或 navigate('/list', { replace: true });
};

// ④ 保持查询参数地跳转
const [searchParams] = useSearchParams();
navigate(`/detail/${id}?${searchParams.toString()}`);

// ⑤ 在「非组件」的地方跳转(如 axios 拦截器)
// ❌ Hooks 只能在组件/Hook 里用 → 需要把 navigate 传出去或建立「导航服务」
// ✅ 方案一:在根组件注册
let globalNavigate: ReturnType<typeof useNavigate> | null = null;

function NavigationBridge() {
  globalNavigate = useNavigate();
  return null;
}

// 拦截器里
if (res.status === 401) {
  globalNavigate?.('/login', { replace: true });
}

// ✅ 方案二:用事件 + 组件监听
window.dispatchEvent(new CustomEvent('auth:expired'));

六、导航阻塞(useBlocker)

// ⚠️ 注意:useBlocker 只在【数据路由】(createBrowserRouter)下可用
import { useBlocker, Form } from 'react-router-dom';

function EditForm() {
  const [dirty, setDirty] = useState(false);

  const blocker = useBlocker(
    ({ currentLocation, nextLocation }) =>
      dirty && currentLocation.pathname !== nextLocation.pathname
  );

  return (
    <>
      <Form>{/* ... */}</Form>

      {blocker.state === 'blocked' && (
        <Modal>
          <p>有未保存的修改,确定离开吗?</p>
          <button onClick={() => blocker.proceed()}>离开</button>
          <button onClick={() => blocker.reset()}>留下</button>
        </Modal>
      )}
    </>
  );
}
// 只阻止「站内导航」;浏览器前进/后退需要额外处理
useEffect(() => {
  const handler = (e: BeforeUnloadEvent) => {
    if (!dirty) return;
    e.preventDefault();                    // ⭐ 触发浏览器原生确认
  };
  window.addEventListener('beforeunload', handler);
  return () => window.removeEventListener('beforeunload', handler);
}, [dirty]);

七、常见坑

// ① 渲染期间调用 navigate
function Bad() {
  navigate('/x');                          // ❌ 副作用发生在渲染阶段
}

// ② 登录成功没加 replace → 后退回到登录页
navigate('/dashboard');                    // ⚠️
navigate('/dashboard', { replace: true }); // ✅

// ③ 用 state 传递「需要分享」的数据
navigate('/detail', { state: { id } });    // ⚠️ 刷新/分享后丢失
navigate(`/detail/${id}`);                 // ✅ 放 URL

// ④ 相对路径用错(以为总是相对根路径)
navigate('settings');                      // ⚠️ 相对当前路由,不是根
navigate('/settings');                     // ✅ 绝对路径

// ⑤ navigate(-1) 在「没有历史」时无效(用户直接打开链接)
navigate(-1);                              // ⚠️ 可能什么都不发生
// ✅ 兜底:navigate(-1) 但检查 location.key === 'default' 时改为 navigate('/')

// ⑥ 在事件处理里「先跳转再 await」
const handle = async () => {
  navigate('/next');                       // ⚠️ 组件卸载后仍 await 后续逻辑
  await save();                            // → 可能在已卸载组件里 setState
};
// ✅ 先 await 再 navigate

// ⑦ 组件卸载后调用 navigate(React 18 不会警告,但逻辑可能不对)
// ⑧ 用 <a href> 代替 navigate(整页刷新)
// ⑨ 在 useEffect 里 navigate 但依赖写错 → 只在挂载时跳一次(可能符合预期,也可能不是)
// ⑩ 「路由跳转」与「状态更新」混在一起导致「更新被丢弃」
// ⑪ 在 axios 拦截器里直接 import useNavigate(Hooks 不能在组件外调用)
// ⑫ 忘记「navigate 会让组件卸载」→ 之后的 setState 无意义
// ⑤ 的兜底写法
function useSafeBack() {
  const navigate = useNavigate();
  const location = useLocation();

  return useCallback(() => {
    // location.key === 'default' 表示「这是会话中的第一个条目」(没有可后退的历史)
    if (location.key === 'default') {
      navigate('/', { replace: true });
    } else {
      navigate(-1);
    }
  }, [navigate, location.key]);
}

八、面试延伸

  1. 「怎么做编程式导航?」
    用 useNavigate() 拿到 navigate 函数,然后在「事件处理、异步回调、Effect」里调用它:navigate('/path') 跳转、navigate(-1) 后退、navigate('/path', { replace: true }) 替换历史、navigate('/path', { state }) 传状态。渲染时的条件重定向不要用 navigate(那是副作用),应该用 <Navigate to="..." replace /> 组件。

  2. 「replace 有什么用?」
    replace: true 表示「替换当前的历史记录」而不是「压入新的一页」。典型场景是登录/注册成功后跳转——如果不加 replace,用户点「后退」会回到登录页(体验很差,而且可能触发「已登录还显示登录页」的怪异状态)。同理「表单提交成功后的跳转」「重定向」也应该用 replace。

  3. 「navigate 的 state 和查询参数有什么区别?」
    ① state 存在「history 条目」里,不进入 URL——适合传「临时数据、来源页面、敏感信息」;优点是无需序列化、刷新后仍在;缺点是「不能分享」「直接打开链接时是 undefined」(所以必须有兜底)。② 查询参数(?a=1) 是 URL 的一部分——适合传「可分享、可刷新恢复」的状态(如筛选条件、页码)。判断标准是「这个状态是否需要被分享/收藏」。

  4. 「相对路径导航怎么用?」
    相对路径是「相对于当前路由」的:navigate('detail') 会跳到「当前路径的子路径」,navigate('..') 跳到「上一级」,navigate('../..') 上两级。这在嵌套路由里很有价值——比如在 /users/123/orders 里想跳到 /users/123/orders/detail,只需 navigate('detail'),不用拼接绝对路径。注意 <Link> 还有 relative="route"(默认,相对路由)/ "path"(相对路径)的区分。

  5. 「为什么不能在渲染中调用 navigate?」
    因为「跳转」是一个副作用(它会改变 URL、触发路由变化与组件卸载/挂载)。而渲染阶段(render phase)必须是纯的——React 可能重复执行它、也可能丢弃它。在渲染中调用 navigate 会导致「不可预期的多次跳转」甚至「无限循环」(跳转 → 重新渲染 → 又跳转)。渲染时的重定向用 <Navigate />,命令式跳转放到事件处理或 useEffect 里。

  6. 「怎么实现『登录后跳回原页面』?」
    两步:① 在「路由守卫」里把「当前位置」放进 state:<Navigate to="/login" state={{ from: location }} replace />;② 登录成功后从 location.state.from 取出原路径并跳过去(同时用 replace: true 避免留下登录页历史):navigate(from?.pathname ?? '/dashboard', { replace: true })。这是一个非常常见的实战模式。

  7. 「navigate(-1) 有什么坑?」
    最大的坑是「没有历史记录时它什么都不做」——如果用户是「直接打开链接」或「从外部跳进来」的,后退可能会退出应用或没有反应。稳妥的做法是判断 location.key(React Router 用它标识历史条目的唯一性,值为 'default' 表示「会话中的第一个条目」):如果是 'default' 就跳到一个明确的兜底路径(如首页),否则才 navigate(-1)。

  8. 「怎么在 React 之外(如 axios 拦截器)做跳转?」
    因为 Hooks 只能在组件/Hook 内调用,所以需要「桥接」。两种做法:① 全局变量持有 navigate——在一个根组件里 globalNavigate = useNavigate(),然后在拦截器里调用 globalNavigate?.('/login')(简单但有点「副作用式」);② 事件总线——拦截器里 window.dispatchEvent(new CustomEvent('auth:expired')),由一个根组件监听并执行跳转(更干净、更可测)。也有方案是「用 store 存一个 redirect 状态,由路由组件响应」。

一句话速记

编程式导航用 useNavigate()(navigate('/path') / navigate(-1) / navigate('/path', { replace: true, state }) / 相对路径 'detail'、'..');渲染时的重定向用 <Navigate replace />(绝不能在渲染中调用 navigate);登录成功、提交成功后要加 replace(否则后退回到登录页);state 传不进 URL 的临时数据(刷新仍在但不可分享,必须有兜底),需要分享的状态放 URL;navigate(-1) 在「无历史」时无效(用 location.key === 'default' 兜底);先 await 再 navigate;React 之外跳转要「全局持有 navigate」或「事件总线」桥接;useBlocker 只在数据路由下可用(未保存提示)。

赞(0) 打赏
未经允许不得转载:陌上寒 » 6.5 编程式导航

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫