tRPC + Next.js 请求取消实战:使用abortOnUnmount在组件卸载时中止 Procedure 调用
【免费下载链接】trpc🧙♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc
在 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/client。createTRPCNext通过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, }; }, });这段代码说明了两件事:
abortOnUnmount是config()返回对象中的一个合法字段,它和url、links、transformer等客户端配置并列;- 它是可选字段,类型为
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 原生选项(enabled、staleTime、refetchInterval等)冲突。需要强调的是:abortOnUnmount是逐请求求值的——同一个组件内不同的查询可以有不同的取值,互不影响。
底层的三级优先级:请求级 > 客户端配置 > Provider 默认值
从源码看,"到底要不要取消"并不是简单的一处布尔判断,而是按明确优先级逐层解析的。以useQuery的实现为例,createHooksInternal.tsx 中的解析逻辑如下:
const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? config?.abortOnUnmount ?? abortOnUnmount;解析优先级从高到低是:
opts.trpc.abortOnUnmount——当前这个 hook 调用传入的请求级选项,优先级最高;config.abortOnUnmount——config()返回的客户端配置(在 React Query 的 queryDefaults 层面解析);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 中止
配置解析只是第一步,真正有价值的问题是:打开这个开关后,请求是如何被取消的?
答案藏在useQuery的queryFn构造逻辑中。当shouldAbortOnUnmount为true时,tRPC 会把 React Query 传入queryFunctionContext的signal塞进 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, );链路可以完整地概括为四步:
- React Query管理查询生命周期。组件卸载时,React Query 会调用其内部
AbortController中止该查询,并通过queryFunctionContext.signal通知数据获取函数; - tRPC 中转。
useQuery/useInfiniteQuery/useSuspenseQuery/usePrefetchQuery等 hooks 依据上文的三级优先级算出shouldAbortOnUnmount,决定是否把这个signal放入传给client.query的选项里; - 客户端发起请求。
@trpc/client收到带signal的请求选项后,在底层 HTTP link(如httpBatchLink/httpLink)中将其绑定到fetch的请求信号上; - 网络层中止。一旦 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()里统一开或关,再在个别useQuery的trpc.abortOnUnmount上做反向覆盖,是兼顾整洁与弹性的组合用法; - 留意 SSR 阶段:
config()在客户端与 SSR 都可能执行,Provider 传入时对空值做了?? false兜底(withTRPC.tsx)。SSR prepass 阶段(ssr: true配合ssrPrepass)的请求由服务端预取流程管理,本选项主要面向客户端卸载场景。
一句话收束:abortOnUnmount是 tRPC × React Query 在 Next.js 中把"查询生命周期"与"组件生命周期"对齐的官方开关,理解它的三级优先级与 signal 透传链路,你就能在不引入额外请求管理库的前提下,精确控制每次 RPC 调用的取消时机。
【免费下载链接】trpc🧙♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考