news 2026/9/9 19:39:02

tRPC v11 useMutation 实战指南:在 @trpc/react-query 中发起端到端类型安全的写操作

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
tRPC v11 useMutation 实战指南:在 @trpc/react-query 中发起端到端类型安全的写操作

tRPC v11 useMutation 实战指南:在 @trpc/react-query 中发起端到端类型安全的写操作

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

本指南围绕 useMutation.md 展开,系统讲解如何在 React 应用中使用@trpc/react-query暴露的useMutation()Hook 调用 tRPC 后端定义好的 mutation 过程(procedure)。它教你完成从服务端 mutation 路由的定义、客户端类型化 Hook 的生成,到用mutate/mutateAsync触发请求、读取isPending/error/data状态、以及成功回调的完整闭环。读完本指南,你将掌握在登录、表单提交、增删改等写操作场景中落地“端到端类型安全”的标准姿势,并理解其底层的 mutation key 与回调包装机制。

@trpc/react-query中的这些 Hook 本质上是@tanstack/react-query(v5)之上的一层薄封装——它替你接好了类型系统与网络调用,而选项、状态机与缓存语义则完整继承自 TanStack Query。因此下文在讲解过程中会同时交代两者的边界,方便你在需要时无缝阅读更底层的行为细节。

前置准备:把 tRPC 客户端接入 React

要使用useMutation(),请先确保项目已经完成 setup.mdx 中的接入步骤:安装@trpc/client@trpc/react-query@tanstack/react-query,用createTRPCReact<AppRouter>()创建类型化的trpc对象,并通过<trpc.Provider>QueryClientProvider包裹应用。

创建类型化客户端的标准写法如下:

// utils/trpc.ts import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact<AppRouter>();

createTRPCReact接收路由器类型作为泛型参数,在 createTRPCReact.tsx 中通过DecorateRouterRecord/DecorateProcedure把路由树中每个过程(procedure)“装饰”成带useQueryuseMutation等 Hook 的代理对象。也就是说,只要后端appRouter里的类型是正确的,trpc.login.useMutation()之类的调用就会被自动类型化,无需手工编写任何接口请求层代码。

第一步:在服务端定义一个 mutation 过程

useMutation()的目标是后端通过.mutation()声明的过程。在 tRPC 中,创建 mutation 的语法与创建 query 几乎一致——区别仅在于把.query()换成.mutation(),并使用.input()配合 zod(或其他验证器)校验并推断入参。

// server/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); export const appRouter = t.router({ // 在 'login' 路径上创建过程 // 声明语法与创建 query 完全相同 login: t.procedure // 使用 zod schema 校验并推断输入值 .input( z.object({ name: z.string(), }), ) .mutation((opts) => { // 在这里执行真正的登录逻辑 return { user: { name: opts.input.name, role: 'ADMIN' as const, }, }; }), }); export type AppRouter = typeof appRouter;

要点说明:

  • opts.input的类型由.input()中的 zod schema 自动推断,在回调体内已是非undefined的、通过校验的值;
  • .mutation()的返回值即客户端data中拿到的响应,整个链路输入输出全部有静态类型保证;
  • appRouter的类型会被导出并喂给客户端的createTRPCReact<AppRouter>(),这是“端到端类型安全”的起点。

第二步:在组件中用 useMutation() 发起写操作

拿到类型化的trpc后,在任意组件内即可通过trpc.<路由路径>.useMutation()获得对应 mutation 的封装 Hook。以下是一个完整可运行的登录表单组件:

// components/MyComponent.tsx import React from 'react'; import { trpc } from '../utils/trpc'; export function MyComponent() { const mutation = trpc.login.useMutation(); const handleLogin = () => { const name = 'John Doe'; mutation.mutate({ name }); }; return ( <div> <h1>Login Form</h1> <button onClick={handleLogin} disabled={mutation.isPending}> Login </button> {mutation.error && <p>Something went wrong! {mutation.error.message}</p>} </div> ); }

运行流程:

  1. 点击按钮 →mutation.mutate({ name }),入参对象必须符合服务端 zod schema({ name: string }),否则编译期直接报错;
  2. 请求进行期间mutation.isPending === true,按钮被禁用;
  3. 失败时mutation.error非空,其类型为TRPCClientErrorLike<AppRouter>,包含经 tRPC 标准化后的错误信息(message等);
  4. 成功时mutation.data即为后端.mutation()返回的对象(此处为{ user: { name, role } })。

mutate 与 mutateAsync:同步触发与异步等待

useMutation()的返回值提供两个触发方法,语义来自 TanStack Query,但入参、返回数据类型均由 tRPC 注入:

  • mutation.mutate(variables):立即发起请求并返回void。适合事件回调场景(如onClick),配合onSuccess/onError/onSettled回调处理结果。
  • mutation.mutateAsync(variables):返回一个 Promise,resolve 后可直接拿到data。适合需要await结果的逻辑(如表单校验后串行提交),也便于与try/catchuseEffect组合:
const handleSubmit = async () => { try { const data = await mutation.mutateAsync({ name: 'John Doe' }); console.log('登录成功,用户角色:', data.user.role); } catch (err) { console.error('登录失败', err); } };

返回值字段速查表

trpc.login.useMutation()的返回值是TRPCHookResult & UseMutationResult<TOutput, TError, TInput, TContext>(见 hooks/types.ts#L305-L314)。除 tRPC 附加的trpc.path外,其余字段均与 TanStack Query v5 的UseMutationResult一致:

字段类型/取值含义
mutate/mutateAsync(variables) => void / Promise<TOutput>触发写操作
dataTOutput \| undefined成功后后端返回的数据
errorTError \| null失败时的错误对象
isPendingboolean请求是否进行中(v5 中替代旧版isLoading
isSuccessboolean最近一次是否成功
isErrorboolean最近一次是否失败
status'idle' \| 'pending' \| 'success' \| 'error'生命周期状态
variablesTInput \| undefined最近一次提交的入参
reset() => void将状态重置为初始idle态,清空data/error
trpc.pathstring本次调用对应的过程路径,如'login'(由 TRPCHookResult 注入)

注意:v5 起mutate抛出的错误属于“未被捕获的 Promise 拒绝”,更推荐使用回调或mutateAsync捕获错误;isPending是 v5 中判断“进行中”的推荐字段(原文档示例即用它禁用按钮)。

常用配置选项:onSuccess、onError 与乐观更新

useMutation()可接收一个可选的配置对象,其类型为:

UseTRPCMutationOptions<TInput, TError, TOutput, TContext>

该类型在 hooks/types.ts#L154-L160 中定义为继承 TanStack Query 的UseMutationOptions<TOutput, TError, TInput, TContext>,即:返回值类型是后端输出,错误类型是TRPCClientErrorLike,变量类型是过程入参——因此所有回调的入参都会被自动类型化,配置对象整体也处于编译期保护之下。

const mutation = trpc.addPost.useMutation({ // 请求发起前同步执行,常用于乐观更新 onMutate: async (variables) => { console.log('准备新增文章:', variables.title); }, // 成功后回调(可在此失效化相关查询,见下文) onSuccess: (data, variables, context) => { console.log('新增成功,新 id 为:', data.id); }, onError: (error, variables, context) => { console.error('失败:', error.message); }, onSettled: (data, error, variables, context) => { console.log('无论成败都会执行'); }, // 失败自动重试次数;写操作默认不重试 retry: false, });

泛型参数TContext专为乐观更新设计:在onMutate中返回上下文对象(如被修改前的旧数据),该对象会原样传递到onError/onSuccess/onSettledcontext参数中,配合queryClient.cancelQueriessetQueryData可实现回滚,具体模式与 useQuery.md 中的缓存约定保持一致。

成功后的数据同步:失效化相关查询

服务端数据被 mutation 改变后,最常见的诉求是让依赖它的 query 重新拉取。tRPC 提供useUtils()(旧版名为useContext,已被标记弃用,见 createTRPCReact.tsx)获取一组工具方法,其中invalidate()可按过程路径定向失效:

import { trpc } from '../utils/trpc'; function PostCreator() { const utils = trpc.useUtils(); const addPost = trpc.addPost.useMutation({ onSuccess: () => { // 新增完成后,让 'post.list' 查询自动重新拉取 utils.post.list.invalidate(); }, }); return ( <button onClick={() => addPost.mutate({ title: 'Hello' })}> {addPost.isPending ? '提交中…' : '新增文章'} </button> ); }

完整的失效化、预取与缓存读写方法族可参阅 useUtils.mdx。

源码视角:useMutation 到底包装了什么?

从 createHooksInternal.tsx#L320-L358 的实现可以看到,useMutation做了四件关键的事:

  1. 构造 mutation keygetMutationKeyInternal(path)把 tRPC 的过程路径(如'login')转成一个确定性的查询键,并透传给 TanStack 的useMutation作为mutationKey
  2. 合并默认配置queryClient.defaultMutationOptions(queryClient.getMutationDefaults(mutationKey)),因此可以用queryClient.setMutationDefaults按路径预设默认回调(测试见 mutationkey.test.tsx、queryClientDefaults.test.tsx);
  3. 注入网络调用:把真正的 RPC 请求封装成 TanStack 的mutationFn,内部调用底层客户端client.mutation(...getClientArgs([path, { input }], opts)),从而复用 tRPC client 的链接(links)体系;
  4. 包装 onSuccess 并暴露路径:success 回调会经mutationSuccessOverride处理(见createRootHooksconfig?.overrides?.useMutation?.onSuccess ?? ((options) => options.originalFn())),并把{ trpc: { path } }追加到 Hook 返回值上。
// 经过简化的内部实现示意(源自 createHooksInternal.tsx 的 useMutation) const mutationKey = getMutationKeyInternal(path); // ① 由过程路径生成 key const defaultOpts = queryClient.defaultMutationOptions( queryClient.getMutationDefaults(mutationKey), // ② 合并 setMutationDefaults ); const hook = __useMutation( { ...opts, mutationKey, mutationFn: (input) => client.mutation(...getClientArgs([path, { input }], opts)), // ③ 走 tRPC client onSuccess(...args) { const originalFn = () => opts?.onSuccess?.(...args) ?? defaultOpts?.onSuccess?.(...args); return mutationSuccessOverride({ originalFn, queryClient, meta: /* 合并后的 meta */ }); }, }, queryClient, );

与此同时,类型层的接入点位于 createTRPCReact.tsx#L353-L370:DecoratedMutation声明了每个 mutation 过程都具备的useMutation<TContext = unknown>(opts?)签名,并用TDef['input']TDef['output']TRPCClientErrorLike<TDef>精确约束入参、返回数据与错误类型;DecorateProcedure则保证只有后端声明为.mutation()的过程才会被装饰出useMutation,query 与 mutation 的能力不会互相串台。

此外,@trpc/react-query还导出基于多态的mutationLike(mutationLike.ts)等类型工具,允许在声明式组件中把“某个带useMutation的过程”作为普通 props 传入并保持类型不丢失,相关行为在 polymorphism.test.tsx 中有大量覆盖(含mutateAsyncisPending的端到端验证)。

进阶:用 createTRPCReact 的 overrides 做全局 onSuccess

当多个 mutation 都希望复用同一份成功处理逻辑(例如“成功后统一失效所有查询”)时,不必在每个组件里重复写回调,可以在创建 tRPC React 对象时通过overrides.useMutation.onSuccess注入全局拦截器:

const trpc = createTRPCReact<AppRouter>({ overrides: { useMutation: { async onSuccess(opts) { // opts.originalFn() 会继续调用组件内(或 mutation defaults 里)定义的 onSuccess if (!opts.meta['skipInvalidate']) { await opts.originalFn(); await opts.queryClient.invalidateQueries(); } }, }, }, });

回调收到的opts对象包含originalFnqueryClientmeta三要素(meta为调用方传入的meta与默认meta的合并结果),典型落地用法可参考仓库测试 overrides.test.tsx —— 它演示了“每次 mutation 成功后清空查询缓存”的全局策略,也可按meta标记(如skipInvalidate)跳过特定调用。

小结与常见注意事项

useMutation是从“类型化声明”到“写操作执行”的关键一环。实践中的几条经验:

  • 写过程用.mutation(),读过程用.query(),不要混用;mutation 默认不做自动重试、不进入 SSR 预取,这与它的写语义相匹配;
  • 判断进行中状态优先使用isPending(v5 语义),而不是旧版习惯的isLoading
  • 依赖事件回调拿结果优先mutate,需要在异步流程中串联后续逻辑时用mutateAsync
  • mutation key 由过程路径推导,所以用queryClient.setMutationDefaults或全局overrides时,只要路径一致即可命中——这正是 tRPC 薄封装设计想要表达的:类型与路由由 tRPC 保证,缓存与状态机交给 TanStack Query,两者各司其职

如果你还想了解 query 侧(useQuery/useInfiniteQuery/useQueries)的完整用法,或服务端查询工具集(useUtils),可继续阅读 client/react 目录 下的对应文档;所有 Hook 的实现与测试均可在仓库packages/react-query/中查证。

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

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

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

Parallels Desktop 27 安装指南:Mac 上运行 Windows 11 与 prlctl 命令行管理

在 Mac 上装 Windows 虚拟机&#xff0c;Parallels Desktop 基本是绕不开的名字。这次我们直接看最新版 Parallels Desktop 27 的安装流程&#xff0c;以及很多人最关心的“一行命令”用法——不是让你去找各种来路不明的修改包&#xff0c;而是走官方正式版安装&#xff0c;再…

作者头像 李华
网站建设 2026/9/9 19:37:56

Python内存管理详解:从引用计数到垃圾回收与调优

我最近帮同事排查一个爬虫任务&#xff0c;脚本本身写得没毛病&#xff0c;但跑上七八个小时之后内存占用一路飙到十几个G&#xff0c;最后直接OOM被杀。查来查去发现既不是数据量真的那么大&#xff0c;也不是第三方库泄漏&#xff0c;问题出在一组互相引用的对象上。这让我觉…

作者头像 李华
网站建设 2026/9/9 19:36:35

YOLOv5单目测距实战:从原理到代码实现

简介&#xff1a;YOLOv5与单目测距相结合的Python项目&#xff0c;面向计算机视觉开发者、自动驾驶及机器人领域从业者&#xff0c;解决单摄像头场景下目标检测与距离估计问题&#xff0c;无需激光雷达等额外深度设备&#xff0c;适合智能监控、无人机避障、辅助驾驶等落地需求…

作者头像 李华
网站建设 2026/9/9 19:36:26

Python构建真实AI代理:Agentic AI工程实践全解析

这次我们来看一个很典型的工程向主题&#xff1a;使用 Python 构建真实 AI 代理的 Agentic AI Engineering。注意标题里的三个关键词&#xff1a;真实、AI 代理、工程。也就是说&#xff0c;这本书/课程不是给你讲大模型 API 怎么调&#xff0c;也不是给你看几个 ChatBot Demo&…

作者头像 李华
网站建设 2026/9/9 19:35:32

教育学硕士亲测:智能排版 10 分钟搞定论文格式的完整流程

读教育学硕士的第三年&#xff0c;帮导师整理过十几份学生论文&#xff0c;最深的体会是&#xff1a;内容再好&#xff0c;格式乱了就先输一半。标题字号不统一、图表编号对不上、参考文献一会儿 GB/T 7714 一会儿自创格式&#xff0c;页眉页码更是重灾区。教育学院的格式细则足…

作者头像 李华
网站建设 2026/9/9 19:34:38

SEO交易全解析:从关键词包年到老域名买卖

搜一下“SEO交易”&#xff0c;十个人里有八九个人第一反应是“花钱请人做关键词排名”。这个理解没毛病&#xff0c;但把视角拉远一点&#xff0c;你会发现这个行业里的交易形态远比“找服务商做优化”要丰富得多——有人按月接网站托管&#xff0c;有人按关键词包年卖排名&am…

作者头像 李华