首页
/ React Router Pending UI:构建导航加载与表单提交期间的即时反馈界面

React Router Pending UI:构建导航加载与表单提交期间的即时反馈界面

2026-09-07 17:19:31作者:秋泉律Samson

导航到一个新路由、向 action 提交数据,都需要等待数据返回,用户在这段等待期内会感到"卡住"。本指南基于 React Router 官方文档中的 Pending UI 指南(及对应的 Data Mode 入口文档),系统讲解如何借助 useNavigationNavLink 渲染属性与 useFetcher,在全局导航、局部链接与表单提交三种场景下实现加载中(Pending)与乐观(Optimistic)UI。读完本文,你将掌握判断"正在导航/正在提交"的标准写法,并能在数据尚未落库时就地渲染预期的界面状态。

核心思想:把"等待中的 UI"交给应用代码

React Router 在页面数据就绪前不会渲染新页面——当用户导航到新 URL 时,目标页面的 loader 会在下一帧渲染前被完整执行(参见 Data Loading 中"loaders 在路由组件渲染前被调用"的说明);同理,action 完成后所有 loader 数据会自动重新校验以保持 UI 与数据同步(参见 Actions)。

因此,React Router 本身并不阻塞交互,而是把"等待期间如何反馈"完全交给应用代码负责:

When the user navigates to a new route, or submits data to an action, the UI should immediately respond to the user's actions with a pending or optimistic state. Application code is responsible for this.

框架模式与 Data 模式(createBrowserRouter + RouterProvider)在此处的 API 完全一致——Data Mode 下的 Pending UI 文档 明确指出:"Pending UI is the same as Framework Mode",并指向 Framework 模式指南。而根据 Modes 模式选择文档 中的 API 可用性表,useNavigationuseFetcherNavLinkisPending 能力均同时适用于 Framework 与 Data 模式(Declarative 模式只提供基础的 NavLink,不具备 pending 状态)。如果你尚未选择模式,可先阅读 Picking a Mode

全局 Pending 导航:useNavigation 驱动顶部加载条

最典型的 pending UI 是"全局加载指示器":无论用户点哪个链接进入哪个页面,只要数据加载尚未完成,就在根布局顶部显示一个旋转或进度条。

判断导航是否在进行

useNavigation 返回当前的 Navigation 对象,其 state 分为三种(定义见 NavigationStates):

状态 含义 关键字段
idle 空闲,无进行中的导航 locationundefined,所有 form* 字段亦为 undefined
loading 正在加载目标路由数据 location 指向新位置;若本次加载源自表单 GET 提交,则 formActionformData 等亦可用
submitting 正在提交数据(如 POST 表单) locationformMethodformActionformData 均已就位

实现细节:从 源码 hooks.tsx 可以看出,useNavigation 内部从 router state 中取出 navigation 并剔除 matcheshistoryAction 后以 useMemo 返回;navigation.state"idle" 时不会触发多余渲染,因此可以直接在根组件无条件调用。

在框架模式的路由模块根布局(root.tsx)中,经典写法是用"是否有 navigation.location"作为导航中的判定,因为 idle 时 location 一定为 undefined

import { useNavigation } from "react-router";

export default function Root() {
  const navigation = useNavigation();
  const isNavigating = Boolean(navigation.location);

  return (
    <html>
      <body>
        {isNavigating && <GlobalSpinner />}
        <Outlet />
      </body>
    </html>
  );
}

这段代码的语义是:只要存在一个进行中的导航(无论 loading 还是 submitting),navigation.location 就非空,于是渲染 <GlobalSpinner />;导航结束后自动消失。它放在 <Outlet /> 外层,因此对所有路由生效。

局部 Pending 导航:NavLink 渲染属性实现链接级反馈

当用户点击侧边栏/导航栏中的链接时,等待的新页面还未出现,此时被点击的那个链接应当立刻处于 pending 视觉态(例如变灰或打转),让用户明确感知"这个导航被接受了,正在加载"。

NavLink 的三个 prop——childrenclassNamestyle——都支持传入接收 pending 状态的渲染函数。除了框架模式指南中展示的 childrenstyle 两种用法,className 也可用于批量切换 CSS 类:

import { NavLink } from "react-router";

function Navbar() {
  return (
    <nav>
      <NavLink to="/home">
        {({ isPending }) => (
          <span>Home {isPending && <Spinner />}</span>
        )}
      </NavLink>
      <NavLink
        to="/about"
        style={({ isPending }) => ({
          color: isPending ? "gray" : "black",
        })}
      >
        About
      </NavLink>
      <NavLink
        to="/settings"
        className={({ isPending }) =>
          isPending ? "pending" : "normal"
        }
      >
        Settings
      </NavLink>
    </nav>
  );
}

要点:

  • 只有 data/framework 模式的导航才具备 pending 语义,isPending 在 Declarative 模式下不生效(见 modes.md 可用性表);
  • pending 状态仅作用于触发本次导航的链接本身,不会波及整个导航栏;
  • 若配合 <Link prefetch>/discover 等能力,isPending 的触发时机还与路由数据预取策略相关,属于 Framework 模式专属特性。

Pending 表单提交:按钮在等待期显示"提交中..."

表单提交有两种形态:普通 <Form>(引发一次全局导航)与 <fetcher.Form>(独立状态、不引发导航)。两者的 pending 状态来源不同,需要分别处理。

推荐:fetcher.Form + fetcher.state

由于 useFetcher 返回的 fetcher 拥有独立于全局导航的状态(提交/加载/空闲各自管理),它是实现按钮 pending 态最简单的方式——详见 actions.md 中 "Calling actions with a fetcher" 的"不产生新历史记录条目的提交"说明:

import { useFetcher } from "react-router";

function NewProjectForm() {
  const fetcher = useFetcher();

  return (
    <fetcher.Form method="post">
      <input type="text" name="title" />
      <button type="submit">
        {fetcher.state !== "idle"
          ? "Submitting..."
          : "Submit"}
      </button>
    </fetcher.Form>
  );
}

fetcher.state 三态定义参见 FetcherStates

  • idle:未在调用任何 loader 或 action;
  • loading:正在通过 fetcher.load() 拉取数据;
  • submitting:正在通过 fetcher.Form/fetcher.submit 提交(此时 fetcher.formData 已可用)。

因为 fetcher 不触发全局导航,多个 fetcher 可并行提交,各自的按钮互不干扰。

普通 <Form>:读取 useNavigation 的 form 字段

不使用 fetcher 的普通表单提交会引发一次全局导航,pending 信息位于 useNavigation 上。此时用 navigation.state 判断过于宽泛(同一时刻只可能有一个全局导航),因此应结合 navigation.formAction 精确判断"本次提交是否发往当前表单的 action":

import { useNavigation, Form } from "react-router";

function NewProjectForm() {
  const navigation = useNavigation();

  return (
    <Form method="post" action="/projects/new">
      <input type="text" name="title" />
      <button type="submit">
        {navigation.formAction === "/projects/new"
          ? "Submitting..."
          : "Submit"}
      </button>
    </Form>
  );
}

当用户提交本表单后,navigation 进入 submitting(随后转入 loading),navigation.formAction 会保持为 /projects/new,直到导航完成回到 idle。这避免了页面上其他导航(如点击链接跳转)误触发本按钮的"提交中"文案。

乐观 UI(Optimistic UI):用 fetcher.formData 抢占渲染

某些交互的"目标终态"在提交那一刻就已确定。例如切换任务完成状态:只要用户点了按钮,其意图(把任务标记为完成/未完成)即已知,不需要等服务端返回就能更新勾选框。这种把预期结果先渲染出来、事后由服务端确认的技法就是乐观 UI,能带来"零等待"的即时反馈。

依据:submitting 状态下的 fetcher 会携带本次提交的完整 FormData(见 FetcherStates.Submitting)。因此可以用"是否存在 fetcher.formData"来区分"正在提交中的值"与"服务端已确认的值":

function Task({ task }) {
  const fetcher = useFetcher();

  let isComplete = task.status === "complete";
  if (fetcher.formData) {
    isComplete =
      fetcher.formData.get("status") === "complete";
  }

  return (
    <div>
      <div>{task.title}</div>
      <fetcher.Form method="post">
        <button
          name="status"
          value={isComplete ? "incomplete" : "complete"}
        >
          {isComplete ? "Mark Incomplete" : "Mark Complete"}
        </button>
      </fetcher.Form>
    </div>
  );
}

执行流程拆解:

  1. 初始渲染:fetcher.formDataundefinedisCompletetask.status
  2. 用户点击:表单以 POST 提交到对应 action,fetcher 进入 submittingfetcher.formData.get("status") 即用户刚选择的布尔值,界面立刻切换为勾选/取消状态并显示反向文案;
  3. 提交期间按钮再次被点击时,value 已基于乐观值反转,操作可连续叠加;
  4. action 完成后数据重新校验(revalidation),若服务端结果与乐观值一致,UI 保持不动;若不一致,则以服务端数据为准自动校正。

在 Data 模式下使用同样的 pending API

如果你的应用使用 createBrowserRouter + <RouterProvider> 构建(Data 模式,详见 Custom Framework 的客户端渲染小节),useNavigationuseFetcherNavLink 的 pending 渲染属性均可直接使用,且行为与框架模式一致——Data 模式的 pending-ui.md 正是这样说明的。框架模式仅额外提供了类型安全的 href、路由模块 API、代码分割与 SSR/SPA/SSG 渲染策略(见 modes.md),pending 状态的数据源在底层并无区别。

小结与可深入路径

  • 全局导航等待 → useNavigation().location
  • 链接级局部反馈 → NavLinkchildren/className/style 渲染函数 + isPending
  • fetcher 表单按钮 → fetcher.state !== "idle"
  • 普通表单按钮 → navigation.formAction === "/path"
  • 即时体验 → 用 fetcher.formData 覆盖现有数据实现乐观 UI。

进一步探索可参考:源码层面见 NavigationStates / FetcherStates 的类型定义useNavigation 实现;API 层面见 useNavigationuseFetcherNavLink 文档;完整能力矩阵见 modes.md。若想了解服务端渲染下的导航与数据加载流程,可继续阅读 Data Mode 自定义框架Testing

登录后查看全文
热门项目推荐
相关项目推荐