首页
/ tRPC + Next.js 请求取消实战:使用 `abortOnUnmount` 在组件卸载时中止 Procedure 调用

tRPC + Next.js 请求取消实战:使用 `abortOnUnmount` 在组件卸载时中止 Procedure 调用

2026-09-08 10:46:01作者:郦嵘贵Just

在 tRPC(10.x)的 Next.js 集成中,页面组件卸载时正在飞行中的 RPC 请求默认不会被取消,而是继续占用网络与服务器资源。本文针对 aborting-procedures.md 展开,讲解如何通过 abortOnUnmount 在「全局」和「单次请求」两个层级开启卸载即中止行为,并结合 @trpc/next@trpc/react-query 的实际源码,说明该选项从配置到真正触发 AbortController 的完整底层链路。读完你将能精确控制 Next.js 页面中 tRPC 查询的生命周期,避免组件卸载后产生无效请求。

为什么默认不取消:先理解 tRPC 与 React Query 的关系

@trpc/next 中,tRPC 的 hooks 建立在 @tanstack/react-query 之上:查询的数据获取、缓存、重试与去重由 React Query 负责,而真正发起 HTTP 调用的则是 @trpc/clientcreateTRPCNext 通过 createRootHooks 生成一套绑定到 tRPC Router 类型的 hooks(见 createTRPCNext.tsx),页面里调用的 trpc.post.byId.useQuery(...) 最终都会落到 React Query 的 useQuery 上。

React Query 组件卸载时默认会把该查询标记为取消,但在 tRPC 的封装中,取消请求需要把 React Query 的取消信号透传给底层 HTTP 客户端才能真正中断网络连接。出于保守设计,tRPC 选择默认不进行卸载取消,文档中的原文说明是:

By default, tRPC does not cancel requests on unmount.

从源码看,这个"不取消"的默认值在 Provider 层级被硬编码为 false。在 createHooksInternal.tsx 中:

const TRPCProvider: TRPCProvider<TRouter, TSSRContext> = (props) => {
  const { abortOnUnmount = false, queryClient, ssrContext } = props;

也就是说,即使不配置任何东西,应用也能正常工作——只是卸载时遗留的请求会继续执行。如果你希望主动终止这类请求,就需要显式打开 abortOnUnmount

全局开启:在 config() 回调中配置 abortOnUnmount

最简单的方式是在创建 createTRPCNext 客户端的 config() 回调中全局开启。这正是文档给出的典型写法:

// @target: esnext
// ---cut---
// @filename: utils.ts
// @noErrors
import { createTRPCNext } from '@trpc/next';

export const trpc = createTRPCNext<AppRouter>({
  config() {
    return {
      // ...
      abortOnUnmount: true,
    };
  },
});

这段代码说明了两件事:

  1. abortOnUnmountconfig() 返回对象中的一个合法字段,它和 urllinkstransformer 等客户端配置并列;
  2. 它是可选字段,类型为 boolean,不传即保持默认 false

在类型层面,该字段被定义在 WithTRPCConfig 中。WithTRPCConfig 由 tRPC 客户端配置 CreateTRPCClientOptions<TRouter> 与 React Query 的 CreateTRPCReactQueryClientConfig 交叉,并额外增加了一个 abortOnUnmount?: boolean 属性(见 withTRPC.tsx):

export type WithTRPCConfig<TRouter extends AnyRouter> =
  CreateTRPCClientOptions<TRouter> &
    CreateTRPCReactQueryClientConfig & {
      abortOnUnmount?: boolean;
    };

运行时,withTRPC 会把这个配置读取出来并传给 <trpc.Provider>。注意其中有两个关键点:

  • 组件树由 WithTRPC 包装,配置在 useState 初始化时只读取一次(config.abortOnUnmount,见 withTRPC.tsx),因此后续热更新配置不会影响已经挂载的 Provider;
  • 传递给 Provider 时对缺失值做了兜底处理:(prepassProps as any).abortOnUnmount ?? false(见 withTRPC.tsx),保证即使配置里漏写了该字段,也不会因为 undefined 导致行为不确定。

提示:当前仓库内 @trpc/next 的最新源码已将该配置项迁移到 createTRPCNext 之上更通用的 hooks 层,但 10.x 版本 config() 内传入的写法保持兼容。若你正在从 10.x 向 v11 迁移,可参考迁移目录中的相关指南(migrate-from-v10-to-v11.mdx)。

单次请求覆盖:在 hook 调用处传入 trpc.abortOnUnmount

全局开启后,如果个别请求需要"特赦"(例如某个轮询查询希望卸载后仍在服务端跑完),或者全局未开启但你只想对某一个查询启用取消,都可以在 hook 选项里通过 trpc 命名空间做请求级覆盖。文档中针对文章详情页 PostViewPage 的示例:

// @target: esnext
// ---cut---
// @filename: pages/posts/[id].tsx
// @noErrors
import { trpc } from '~/utils/trpc';

const PostViewPage: NextPageWithLayout = () => {
  const id = useRouter().query.id as string;
  const postQuery = trpc.post.byId.useQuery({ id }, { trpc: { abortOnUnmount: true } });

  return (...)
}

这里 useQuery 的第二个参数是 React Query 的选项对象,tRPC 把自有选项统一收纳在 trpc 字段下,避免与 React Query 原生选项(enabledstaleTimerefetchInterval 等)冲突。需要强调的是:abortOnUnmount 是逐请求求值的——同一个组件内不同的查询可以有不同的取值,互不影响。

底层的三级优先级:请求级 > 客户端配置 > Provider 默认值

从源码看,"到底要不要取消"并不是简单的一处布尔判断,而是按明确优先级逐层解析的。以 useQuery 的实现为例,createHooksInternal.tsx 中的解析逻辑如下:

const shouldAbortOnUnmount =
  opts?.trpc?.abortOnUnmount ?? config?.abortOnUnmount ?? abortOnUnmount;

解析优先级从高到低是:

  1. opts.trpc.abortOnUnmount——当前这个 hook 调用传入的请求级选项,优先级最高;
  2. config.abortOnUnmount——config() 返回的客户端配置(在 React Query 的 queryDefaults 层面解析);
  3. abortOnUnmount——来自 trpc.Provider 上下文的值,也就是上面 Provider 里默认的 false

其余 hooks 也遵循同样的解析原则。例如 useInfiniteQuery 中为(见 createHooksInternal.tsx):

// request option should take priority over global
const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? abortOnUnmount;

值得注意的是 ??(空值合并)运算符的使用:只有当高层选项为 null/undefined 时才向下一层取值;如果你显式写了 abortOnUnmount: false,它会正确覆盖全局的 true。因此"全局关闭 + 个别开启"与"全局开启 + 个别关闭"两种组合都能精确实现。

信号如何真正生效:从 React Query 的 signal 到 HTTP 中止

配置解析只是第一步,真正有价值的问题是:打开这个开关后,请求是如何被取消的?

答案藏在 useQueryqueryFn 构造逻辑中。当 shouldAbortOnUnmounttrue 时,tRPC 会把 React Query 传入 queryFunctionContextsignal 塞进 tRPC 客户端请求选项;否则显式传 signal: null,确保不继承任何取消能力(见 createHooksInternal.tsx):

const hook = __useQuery(
  {
    ...ssrOpts,
    queryKey: queryKey as any,
    queryFn: async (queryFunctionContext) => {
      const actualOpts = {
        ...ssrOpts,
        trpc: {
          ...ssrOpts?.trpc,
          ...(shouldAbortOnUnmount
            ? { signal: queryFunctionContext.signal }
            : { signal: null }),
        },
      };
      const result = await client.query(...getClientArgs(queryKey, actualOpts));
      // ...
      return result;
    },
  },
  queryClient,
);

链路可以完整地概括为四步:

  1. React Query 管理查询生命周期。组件卸载时,React Query 会调用其内部 AbortController 中止该查询,并通过 queryFunctionContext.signal 通知数据获取函数;
  2. tRPC 中转useQuery/useInfiniteQuery/useSuspenseQuery/usePrefetchQuery 等 hooks 依据上文的三级优先级算出 shouldAbortOnUnmount,决定是否把这个 signal 放入传给 client.query 的选项里;
  3. 客户端发起请求@trpc/client 收到带 signal 的请求选项后,在底层 HTTP link(如 httpBatchLink/httpLink)中将其绑定到 fetch 的请求信号上;
  4. 网络层中止。一旦 signal 被触发,浏览器立即 abort() 底层的 fetch,正在传输的请求被中断,请求也自然不会再更新组件状态(此时组件已卸载)。

换句话说,abortOnUnmount: true 的本质是把 React Query 的查询取消信号桥接给 tRPC 客户端,而取消本身由浏览器/运行时的 AbortController + fetch 机制兜底实现。仓库中针对该行为的回归测试位于 abortOnUnmount.test.tsx,它验证了卸载后请求确实会被中止。

适用面与边界:哪些 hooks 支持、哪些不支持

结合源码中对 abortOnUnmount 的读取位置(createHooksInternal.tsx)可以推断,该开关覆盖了大部分查询类 hooks,包括:

  • useQuery(含 useSuspenseQuery 系列)
  • useInfiniteQuery(含对应的 prefetch 变体)
  • usePrefetchQuery / usePrefetchInfiniteQuery

它们在 Provider 内共享同一个 context.abortOnUnmount,因此开启后行为一致。

同时需要明确两点限制:

  • 变更(mutation)不受该选项控制abortOnUnmount 只作用于卸载后仍可能残留的查询请求;手动触发的 mutation 由 useMutation().mutate() 明确调用,组件卸载时本就不应该用同一个开关静默取消,以免引发状态不一致;
  • 真正的取消依赖网络层支持。该机制最终落实为对 fetch 的 abort。如果你使用的是自定义 transport、SSE/WebSocket 订阅或第三方非标准 fetch 实现,其取消语义取决于对应实现是否监听 signal,不能仅凭该开关保证请求在服务端也会立刻中断。服务端收到中止信号后,过程(procedure)是否停止执行还取决于服务端实现与运行时对连接断开的处理。

最佳实践小结

  • 默认保持关闭是合理选择:并非所有请求都适合卸载即取消——例如后台统计、埋点、或写入型预热的请求可能希望"发出去了就别管";且 React Query 本身对已解析缓存有保护,残留请求一般不引发可见 bug;
  • 全局开启适合"快速离开型"页面:信息流、列表详情这类用户频繁进出、请求耗时长、结果不再被展示的页面,全局 abortOnUnmount: true 能立刻释放连接;
  • 请求级覆盖用于精确治理:在 config() 里统一开或关,再在个别 useQuerytrpc.abortOnUnmount 上做反向覆盖,是兼顾整洁与弹性的组合用法;
  • 留意 SSR 阶段config() 在客户端与 SSR 都可能执行,Provider 传入时对空值做了 ?? false 兜底(withTRPC.tsx)。SSR prepass 阶段(ssr: true 配合 ssrPrepass)的请求由服务端预取流程管理,本选项主要面向客户端卸载场景。

一句话收束:abortOnUnmount 是 tRPC × React Query 在 Next.js 中把"查询生命周期"与"组件生命周期"对齐的官方开关,理解它的三级优先级与 signal 透传链路,你就能在不引入额外请求管理库的前提下,精确控制每次 RPC 调用的取消时机。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393