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)“装饰”成带useQuery、useMutation等 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> ); }运行流程:
- 点击按钮 →
mutation.mutate({ name }),入参对象必须符合服务端 zod schema({ name: string }),否则编译期直接报错; - 请求进行期间
mutation.isPending === true,按钮被禁用; - 失败时
mutation.error非空,其类型为TRPCClientErrorLike<AppRouter>,包含经 tRPC 标准化后的错误信息(message等); - 成功时
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/catch或useEffect组合:
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> | 触发写操作 |
data | TOutput \| undefined | 成功后后端返回的数据 |
error | TError \| null | 失败时的错误对象 |
isPending | boolean | 请求是否进行中(v5 中替代旧版isLoading) |
isSuccess | boolean | 最近一次是否成功 |
isError | boolean | 最近一次是否失败 |
status | 'idle' \| 'pending' \| 'success' \| 'error' | 生命周期状态 |
variables | TInput \| undefined | 最近一次提交的入参 |
reset | () => void | 将状态重置为初始idle态,清空data/error |
trpc.path | string | 本次调用对应的过程路径,如'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/onSettled的context参数中,配合queryClient.cancelQueries、setQueryData可实现回滚,具体模式与 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做了四件关键的事:
- 构造 mutation key:
getMutationKeyInternal(path)把 tRPC 的过程路径(如'login')转成一个确定性的查询键,并透传给 TanStack 的useMutation作为mutationKey; - 合并默认配置:
queryClient.defaultMutationOptions(queryClient.getMutationDefaults(mutationKey)),因此可以用queryClient.setMutationDefaults按路径预设默认回调(测试见 mutationkey.test.tsx、queryClientDefaults.test.tsx); - 注入网络调用:把真正的 RPC 请求封装成 TanStack 的
mutationFn,内部调用底层客户端client.mutation(...getClientArgs([path, { input }], opts)),从而复用 tRPC client 的链接(links)体系; - 包装 onSuccess 并暴露路径:success 回调会经
mutationSuccessOverride处理(见createRootHooks中config?.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 中有大量覆盖(含mutateAsync、isPending的端到端验证)。
进阶:用 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对象包含originalFn、queryClient与meta三要素(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),仅供参考