Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

导航拦截 (Navigation Blocking)

导航拦截是一种阻止导航发生的方法。这在用户尝试导航但处于以下情况时非常典型:

在这些情况下,应该向用户显示提示或自定义 UI,以确认他们是否真的要离开当前页面。

导航拦截是如何工作的?

导航拦截会在整个底层 History API 上添加一层或多层“拦截器 (blockers)”。如果存在任何拦截器,导航将通过以下方式之一暂停:

如何使用导航拦截?

有两种使用导航拦截的方式:

基于 Hook/逻辑的拦截

假设我们想在表单被修改(dirty)时阻止导航。我们可以使用 useBlocker hook:

import { useBlocker } from "@tanstack/react-router";

function MyComponent() {
  const [formIsDirty, setFormIsDirty] = useState(false);

  useBlocker({
    shouldBlockFn: () => {
      if (!formIsDirty) return false;

      const shouldLeave = confirm("你确定要离开吗?");
      return !shouldLeave;
    },
  });

  // ...
}

shouldBlockFn 提供了对 current(当前)和 next(下一个)位置的类型安全访问:

import { useBlocker } from "@tanstack/react-router";

function MyComponent() {
  // 始终拦截从 /foo 到 /bar/123?hello=world 的导航
  const { proceed, reset, status } = useBlocker({
    shouldBlockFn: ({ current, next }) => {
      return (
        current.routeId === "/foo" &&
        next.fullPath === "/bar/$id" &&
        next.params.id === 123 &&
        next.search.hello === "world"
      );
    },
    withResolver: true,
  });

  // ...
}

请注意,即使 shouldBlockFn 返回 false,在页面刷新或关闭标签页时仍可能触发浏览器的 beforeunload 事件。为了控制这一点,你可以使用 enableBeforeUnload 选项来条件性地注册 beforeunload 处理程序:

import { useBlocker } from "@tanstack/react-router";

function MyComponent() {
  const [formIsDirty, setFormIsDirty] = useState(false);

  useBlocker({
    /* ... */
    enableBeforeUnload: formIsDirty, // 或者 () => formIsDirty
  });

  // ...
}

你可以在 API 参考 中找到关于 useBlocker hook 的更多信息。

基于组件的拦截

除了基于逻辑/Hook 的拦截外,你还可以使用 Block 组件来实现类似的结果:

import { Block } from "@tanstack/solid-router";

function MyComponent() {
  const [formIsDirty, setFormIsDirty] = useState(false);

  return (
    <Block
      shouldBlockFn={() => {
        if (!formIsDirty) return false;

        const shouldLeave = confirm("你确定要离开吗?");
        return !shouldLeave;
      }}
      enableBeforeUnload={formIsDirty}
    />
  );

  // 或者

  return (
    <Block
      shouldBlockFn={() => formIsDirty}
      enableBeforeUnload={formIsDirty}
      withResolver
    >
      {({ status, proceed, reset }) => <>{/* ... */}</>}
    </Block>
  );
}

如何显示自定义 UI?

在大多数情况下,在 shouldBlockFn 函数中使用 window.confirm 并将 hook 设置为 withResolver: false 就足够了,因为它能清晰地告知用户导航已被拦截,并根据其响应解析拦截状态。

然而在某些情况下,你可能希望显示一个与应用设计集成得更好、干扰性更小的自定义 UI。

注意: 如果 withResolvertrueshouldBlockFn 的返回值不会直接解析拦截状态。

带解析器 (Resolver) 的 Hook 式自定义 UI

import { useBlocker } from "@tanstack/react-router";

function MyComponent() {
  const [formIsDirty, setFormIsDirty] = useState(false);

  const { proceed, reset, status } = useBlocker({
    shouldBlockFn: () => formIsDirty,
    withResolver: true,
  });

  // ...

  return (
    <>
      {/* ... */}
      {status === "blocked" && (
        <div>
          <p>你确定要离开吗?</p>
          <button onClick={proceed}>是</button>
          <button onClick={reset}>否</button>
        </div>
      )}
    </>
  );
}

不带解析器的 Hook 式自定义 UI

import { useBlocker } from "@tanstack/react-router";

function MyComponent() {
  const [formIsDirty, setFormIsDirty] = useState(false);

  useBlocker({
    shouldBlockFn: () => {
      if (!formIsDirty) {
        return false;
      }

      const shouldBlock = new Promise<boolean>((resolve) => {
        // 使用你选择的模态框管理器
        modals.open({
          title: "你确定要离开吗?",
          children: (
            <SaveBlocker
              confirm={() => {
                modals.closeAll();
                resolve(false); // 不拦截,继续导航
              }}
              reject={() => {
                modals.closeAll();
                resolve(true); // 拦截,取消导航
              }}
            />
          ),
          onClose: () => resolve(true),
        });
      });
      return shouldBlock;
    },
  });

  // ...
}

基于组件的自定义 UI

与 Hook 类似,Block 组件通过 render props 返回相同的状态和函数:

import { Block } from "@tanstack/react-router";

function MyComponent() {
  const [formIsDirty, setFormIsDirty] = useState(false);

  return (
    <Block shouldBlockFn={() => formIsDirty} withResolver>
      {({ status, proceed, reset }) => (
        <>
          {/* ... */}
          {status === "blocked" && (
            <div>
              <p>你确定要离开吗?</p>
              <button onClick={proceed}>是</button>
              <button onClick={reset}>否</button>
            </div>
          )}
        </>
      )}
    </Block>
  );
}