news 2026/9/8 22:53:26

React Router 的 `useTransitions`:在 React 19 并发渲染下正确驱动导航与路由状态更新

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Router 的 `useTransitions`:在 React 19 并发渲染下正确驱动导航与路由状态更新

React Router 的useTransitions:在 React 19 并发渲染下正确驱动导航与路由状态更新

【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router

React 18/19 引入的「Transition」允许你将 UI 更新区分为紧急(urgent)与非紧急(non-urgent),但它的语义与路由这类「需要持续整个导航生命周期」的状态并不天然兼容。本文围绕 React Router 提供的useTransitions可选项,说明默认行为存在的两个坑、如何用false退出startTransition、如何用true接入startTransition+useOptimistic,并深入到packages/react-router源码展示每种行为实际发生的状态更新路径。读完你将能在 Framework / Data / Declarative 三种模式下为自己的应用选择正确的 Transitions 策略。

React Transitions 概念回顾:紧急 vs 非紧急更新

React 18 起,React 引入了「concurrent rendering」,并以此为基础提供 Transitions:你可以在渲染阶段把一个更新标记为「非紧急」,从而让输入响应、动画等紧急更新优先执行,非紧急更新可以在后台渲染并可被中断。相关的核心 API 包括React.useTransitionReact.startTransition

React 19 进一步补全了这一套并发/异步能力:

  • 引入Actions(在 Transition 中使用 async 函数提交动作)与async Transitions,即startTransition(() => Promise)形式的调用可以跨越异步操作持续;
  • 引入新 HookReact.useOptimistic,允许你在 Transition 进行期间先展示一个「乐观」状态,用即时反馈覆盖正在加载的状态,等真实结果到达后再替换。

这些能力是理解 React RouteruseTransitions设计的前提:路由导航本质上是「跨越多次异步状态更新的长期 Transition」,如果不能正确地跨越整个导航,诸如useNavigationuseActionData这类 Hook 在过渡期间将无法工作。

为什么路由导航需要专门的 Transition 处理

React Router 在 v6.13.0 起首次借助React.startTransition让内部路由状态更新更适配 Suspense,方式是通过future.v7_startTransitionfuture flag 开启;进入 v7 后该行为成为默认——所有路由状态更新都会被包裹在React.startTransition中(CHANGELOG 中对应的「Removefuture.v7_startTransitionflag」条目即为这一默认化动作)。

这套默认行为存在两个 React Router 想要用useTransitions解决的潜在问题:

问题一:并非所有场景都适合被startTransition包裹。一个典型场景是React.useSyncExternalStore:它的更新本质上是同步的、无法被包裹成 Transition。当你把来自外部 store 的更新放进 Transition 时,可能会在更新过程中提前渲染 fallback,反而比不包 Transition 表现更差。React Router 为此提供了导航上的flushSync选项(调用ReactDOM.flushSync来执行状态更新),但flushSync并不总是合适的解法——它会放弃并发渲染的收益。

问题二:React 19 的新 API 与旧实现不匹配。React 19 增加了startTransition(() => Promise)useOptimistic。在未升级 React Router 的情况下,startTransition(() => navigate(path))并不会如你期望地工作:因为 Router 内部没有使用useOptimistic,导航期间的路由状态更新不会「浮现」到 UI 上,这直接破坏了useNavigation等 Hook。

为解决以上两个问题,React Router 在路由组件上新增useTransitions属性:用它关闭内部startTransition(解决问题一),或开启startTransition+useOptimistic的增强用法(解决问题二)。从源码实现看,由于默认行为相对 React 19 并不完整,React Router 计划在 v8 中将 opt-in 行为设为默认,同时大概率保留false退出选项以满足useSyncExternalStore这类用例。

三种取值速览:undefined / false / true

useTransitions取值内部状态更新Link/Form 导航useOptimistic 乐观浮现
undefined(缺省)包裹startTransition(v7 默认行为)不自动包裹不使用
false完全不包裹,直接同步更新不包裹不使用
true包裹startTransition自动包裹为 async Transition使用(Framework/Data 模式)

注意:在 Framework 或 Data 模式开启useTransitions={true}要求应用运行在 React 19 上,因为该特性依赖React.useOptimistic。在声明式(Declarative)模式中因为没有数据路由状态,true只代表所有状态更新包裹startTransition,不存在乐观浮现这一层。

三种模式下如何配置useTransitions

useTransitions作为一个布尔属性,被同时加入到了三种模式对应的路由入口组件中:

  • Framework 模式HydratedRouter(通常在entry.client.tsx中渲染);
  • Data 模式RouterProvider(配合createBrowserRouter等数据路由);
  • Declarative 模式BrowserRouterMemoryRouterHashRouter等声明式路由组件。

源码层面,三种模式的接入点保持了一致:RouterProvider会把该值透传给内部<Router useTransitions={...}>(见 components.tsx),并进一步通过NavigationContext暴露给<Link>/<Form>消费(context.ts 中NavigationContext就含useTransitions字段);声明式路由组件则在收到值后直接传给同一个<Router>。Framework 模式的 HydratedRouter 则把props.useTransitions原样传给内部的RouterProvider

服务器渲染(SSR)场景无需 Transition,因此服务端静态渲染路径会固定使用useTransitions={false}(见 server.tsx 中StaticRouter的渲染分支)。

退出 Transition:useTransitions={false}

如果你的应用「不兼容 Transition」——例如重度依赖useSyncExternalStore,或你发现startTransition包裹导致显示不该出现的 fallback——可以通过属性显式退出:

// Framework Mode (entry.client.tsx) <HydratedRouter useTransitions={false} /> // Data Mode <RouterProvider useTransitions={false} /> // Declarative Mode <BrowserRouter useTransitions={false} />

开启后,路由器将不再把内部状态更新包裹进React.startTransition。这一点在源码中有清晰体现:

  • 在 RouterProvider 的 setState 实现 中,状态提交逻辑按序判断:优先使用reactDomFlushSyncImpl(若本次更新要求flushSync);其次useTransitions === false时直接同步调用setStateImpl(newState);否则才进入React.startTransition分支。
  • 声明式路由同样如此,例如 MemoryRouter 的 history 监听回调:useTransitions === false时直接setStateImpl(newState),否则才用React.startTransition包裹。

也就是说,false是一把「绝对同步」的开关,配合导航选项里的flushSync,可以让你完全掌控状态提交时机,代价是放弃并发/Transition 带来的 UI 优先级调度能力。

启用 Transition:useTransitions(React 19)

若你希望应用与 React 19 的并发特性、Actions、Optimistic UI 全面兼容,就开启该属性:

// Framework Mode (entry.client.tsx) <HydratedRouter useTransitions /> // Data Mode <RouterProvider useTransitions /> // Declarative Mode <BrowserRouter useTransitions />

开启之后会发生三件事:

  1. 所有内部状态更新仍包裹在React.startTransition(与不传该属性时的现状一致);
  2. 所有<Link>/<Form>导航被自动包裹在React.startTransition,并且 Transition 会使用useNavigate/useSubmit返回的 Promise,从而让这次 Transition 持续整个导航的完整生命周期,而不是在发起导航后立刻结束;
  3. 在 Framework / Data 模式下,导航期间的部分路由状态会通过React.useOptimistic浮现给 UI

源码里发生了什么:useOptimistic + 选择性合并

useTransitions={true}的核心实现在 RouterProvider 中。组件内部用React.useOptimistic包装了路由状态:

let [_state, setStateImpl] = React.useState(router.state); let [state, setOptimisticState] = useOptimistic(_state);

当一次状态更新到来且useTransitions === true时,更新会先通过setOptimisticState应用由 getOptimisticRouterState 计算出的乐观状态,再执行真正的setStateImpl(newState)

getOptimisticRouterState揭示了「哪些状态被浮现、哪些不被浮现」的精确规则——它把新状态按如下策略合并到当前状态上:

  • 会浮现的「进行中」状态state.navigation(供useNavigation()读取)、state.revalidation(供useRevalidator())、state.actionData(供useActionData())、state.fetchers(供useFetcher()/useFetchers())。其中navigation仅在非idle时用新值、revalidation仅在非idle时用新值、actionData在不是submitting时才用新值,避免把「上一轮的陈旧结果」提前浮现。
  • 不会被浮现的「当前 location 相关」状态state.location(供useLocation)、state.matches(供useMatches())、state.loaderData(供useLoaderData())、state.errors(供useRouteError())等。源码注释明确写道:导航中途不应浮现当前 location 专属的数据。

这种「只提前展示 in-flight 信息、不提前移动 location」的取舍,正是路由场景下乐观 UI 的正确语义:你可以在导航期间立即显示 loading / pending / action 结果,但路由 URL、匹配结果与 loader 数据必须在数据真正就绪后才切换。

Link 与 Form 的自动 async Transition

只有<Link><Form>这两个组件会自动获得 Transition 包裹,其内部都读取了NavigationContext.useTransitions

  • useLinkClickHandler(<Link>内部点击处理器)中,当useTransitions为真时执行React.startTransition(() => doNavigate()),其中doNavigate返回useNavigate调用的结果;
  • Form 的 submitHandler 中,当useTransitions && navigate !== false时执行React.startTransition(() => doSubmit())

之所以能「让 Transition 覆盖整个导航」,是因为数据路由下的useNavigate返回一个 Promise:在 useNavigateStable 中navigate被实现为 async 函数并await router.navigate(...)。将该 Promise 返回给startTransition,React 就会一直保持该 Transition 处于 pending,直到导航真正完成。

手动开启 vs 不开启的 API 对照

<Link><Form>之外,所有操作都需要你自己决定是否包在startTransition里:

// 自动开启 Transition <Link to="/path" /> <Form method="post" action="/path" /> // 手动开启 Transition startTransition(() => navigate("/path")); startTransition(() => submit(data, { method: 'post', action: "/path" })); startTransition(() => fetcher.load("/path")); startTransition(() => fetcher.submit(data, { method: "post", action: "/path" })); // 不开启 Transition(直接调用) navigate("/path"); submit(data, { method: 'post', action: "/path" }); fetcher.load("/path"); fetcher.submit(data, { method: "post", action: "/path" });

这为精细控制留了口子:useNavigate/useSubmit/ fetcher API 本身不会自动包裹startTransition,因此当你在某些交互里希望绕过 Transition、获得同步行为时,直接调用它们即可。

关键约定:必须 return / await navigate 的 Promise

当你手动使用startTransition包裹导航时,必须让 Promise 被返回或被 await,否则 Transition 会在导航真正结束前提前终止,导致乐观状态与 pending UI 失效:

// ✅ 返回了 Promise startTransition(() => navigate("/path")); startTransition(() => { setOptimistic(something); return navigate("/path"); }); // ✅ await 了 Promise startTransition(async () => { setOptimistic(something); await navigate("/path"); }); // ❌ 未返回 Promise —— Transition 提前结束 startTransition(() => { setOptimistic(something); navigate("/path"); }); // ❌ 未 await Promise startTransition(async () => { setOptimistic(something); navigate("/path"); });

这条约定直接对应前述源码事实:useNavigate是 async 函数、返回 Promise,而 React 19 的startTransition正是靠「接收到被返回/await 的 Promise」来延长 Transition 生命周期的。

已知边界:popstate与浏览器后退

当前存在一个与乐观状态和popstate(浏览器前进/后退)相关的已知问题。如果你需要在一次「无法同步完成」的后退导航中读取当前路由(例如因未缓存的数据而 Suspense),应该在导航发生之前先设置乐观状态,或者把乐观更新推迟到 timer / microtask 中执行,以避免读取到不一致的路由信息。

用测试用例验证三种行为

仓库在 react-transitions-test.tsx 中对三档取值做了系统性的行为测试,测试骨架按取值分组:

  • <RouterProvider useTransitions={undefined} />:普通导航/提交能浮现所有更新;同时验证了「手动把导航包进startTransition会出现 buggy 的乐观行为」——这正是默认行为的问题所在,也是引入该 flag 的动机;
  • <RouterProvider useTransitions={false} />:验证导航与提交「不启用 Transition」;
  • <RouterProvider useTransitions={true} />:验证Link导航与Form提交被自动开启 Transition;useNavigate/useSubmit/useFetcher/useRevalidator默认不会自动开启,但可以被手动包进startTransition;以及actionData、fetcher 更新会在 Transition 期间浮现。

这些用例精准对应了本文讨论的 API 边界,可作为你在自己应用中验证行为时的参考。

升级与兼容性建议

  • React 版本useTransitions={true}的完整能力(乐观浮现)需要 React 19;不满足时请继续使用缺省行为或false
  • RSC(React Server Components)场景:从 RouterProvider 实现 可见,RSC 路由上下文会自动强制开启该特性(useTransitions = unstable_rsc || useTransitions),这是内部约定而非需要你手动配置的部分。
  • v8 路线:官方计划在 React Router v8 中将true的增强行为设为默认,同时大概率保留false退出开关,以覆盖useSyncExternalStore等不适用 Transition 的用例。因此现在显式设置useTransitions可以让你在未来的主版本升级中保持对自身行为的完全掌控。

延伸阅读

  • 关联官方文档:React Transitions 说明
  • 组件 API:RouterProvider、HydratedRouter、BrowserRouter
  • 相关 Hook:useNavigation、useRevalidator、useActionData、useFetcher、useFetchers
  • 核心实现:RouterProvider / getOptimisticRouterState、Link / Form / useLinkClickHandler / MemoryRouter、useNavigateStable、服务端静态渲染
  • 行为测试:react-transitions-test.tsx

【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 22:49:29

tiny11builder 完整指南:如何快速打造轻量级 Windows 11 精简镜像

tiny11builder 完整指南&#xff1a;如何快速打造轻量级 Windows 11 精简镜像 【免费下载链接】tiny11builder Scripts to build a trimmed-down Windows 11 image. 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny11builder 受够了十几 GB 的官方安装介质和开机…

作者头像 李华
网站建设 2026/9/8 22:48:54

多学科论文写作难?e稿全学科适配能力覆盖医护理工教育

多学科论文写作的工具适配痛点不同学科的论文写作在专业术语、格式要求、逻辑结构上存在较大差异&#xff0c;通用AI工具存在专业术语错误、逻辑不符合学科规范、适配性差等痛点&#xff0c;需要全学科适配的垂直工具。学术写作的学科属性极强&#xff0c;不同领域的出版规范、…

作者头像 李华
网站建设 2026/9/8 22:46:16

K8s渗透测试工具链实战:从Pod到集群管理员

从Pod到集群管理员&#xff1a;一次完整的K8s渗透测试工具链实战解析最近在做一次授权的K8s渗透测试&#xff0c;目标是一个生产环境中的私有集群。整个测试走完一遍之后&#xff0c;我最大的感触是&#xff1a;K8s环境下&#xff0c;拿到一个Pod的身份往往只是开始&#xff0c…

作者头像 李华