首页
/ tRPC v10 useMutation 全解:用 `@trpc/react-query` 写出端到端类型安全的 mutation

tRPC v10 useMutation 全解:用 `@trpc/react-query` 写出端到端类型安全的 mutation

2026-09-08 09:58:57作者:劳婵绚Shirley

tRPC v10 的 @trpc/react-query 包把服务端 mutation(t.procedure.input(...).mutation(...))暴露成客户端 React Hook trpc.xxx.useMutation()。本篇指南以官方 v10 文档 useMutation.md 为主线,结合仓库中 packages/react-query 的源码实现,讲透「如何定义 mutation → 如何在组件里调用 → 底层是如何被包装为 TanStack Query mutation 的」,让你在业务里写出无 any、天然同步类型、可维护的写操作代码。

核心认知:useMutation 是 TanStack Query mutations 的一层薄封装

v10 文档在开篇就给出了一个极其重要的定位:

@trpc/react-query 提供的 hooks 是对 @tanstack/react-query 的 thin wrapper(薄封装)。关于 options 与使用模式的深度资料,请参考其 mutations 指南。

这句话意味着两件事:

  1. 你在 TanStack Query v4 里学过的所有 mutation 心智模型(mutatemutateAsyncisLoadingstatusonSuccess/onError/onSettledvariablesreset 等)在 useMutation全部适用
  2. tRPC 替你解决的是「类型不落地」的问题——路由路径、输入校验、输出推断都由服务端 AppRouter 一路推导到客户端 Hook,编译器全程把关。

tRPC v11 之后把 React Query 集成拆分为独立的 @trpc/tanstack-react-query 包,但 v10 的这一设计是理解整个演进的基础。本文按 v10 文档对应版本的语义讲解,并特别标注 v11 中的术语变化(例如 isLoading 在 v11 文档示例中改称 isPending),方便你迁移时对照。

一、服务端:先定义一个 mutation procedure

客户端 useMutation 的类型全部来自服务端路由。v10 文档给出了一个最典型的 mutation 定义——登录过程:接收 { name },返回一个用户对象。

import { initTRPC } from '@trpc/server';
import { z } from 'zod';

export const t = initTRPC.create();

export const appRouter = t.router({
  // Create procedure at path 'login'
  // The syntax is identical to creating queries
  login: t.procedure
    // using zod schema to validate and infer input values
    .input(
      z.object({
        name: z.string(),
      }),
    )
    .mutation((opts) => {
      // Here some login stuff would happen
      return {
        user: {
          name: opts.input.name,
          role: 'ADMIN',
        },
      };
    }),
});

几个关键点:

  • 语法与 query 一致:注释中特意强调 “The syntax is identical to creating queries”。mutation 与 query 的唯一区别是把 .query((opts) => ...) 换成 .mutation((opts) => ...),声明在 t.router 下的路径(这里是 login)即客户端的调用路径。
  • opts.input 是校验后的强类型zod schema 既做运行时校验,又通过类型推断让 opts.input.name 在服务端与客户端同时获得 string 类型。
  • 返回类型可推断:返回的 { user: { name, role } } 会被 inferTransformedProcedureOutput 类工具类型捕获,成为客户端 data 的类型。

二、客户端:在组件里调用 trpc.login.useMutation()

v10 文档给出了完整的组件示例,它也是绝大多数 tRPC React 应用里写操作的标准形态:

import { trpc } from '../utils/trpc';

export function MyComponent() {
  // This can either be a tuple ['login'] or string 'login'
  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.isLoading}>
        Login
      </button>

      {mutation.error && <p>Something went wrong! {mutation.error.message}</p>}
    </div>
  );
}

把这段代码拆开看:

片段 作用 类型来源
trpc.login.useMutation() 返回一个受控的 mutation 对象 AppRouterlogin procedure 的输入/输出自动推导
mutation.mutate({ name }) 触发执行;入参必须是 zod 校验过的 { name: string } inferProcedureInput
mutation.isLoading mutation 执行期间为 true,用来禁用按钮防重复提交 TanStack Query mutation 状态机
mutation.error 请求失败时携带 TRPCClientError,可安全访问 error.message TRPCClientErrorLike<AppRouter>

注意注释里提到的一点:路径既可以是字符串 'login',也可以是元组 ['login']。当你的路由是多层嵌套(例如 post.edit,或 ['post', 'edit'])时,两种写法等价——这一点后面会结合代理源码解释。

关于状态命名的版本差异

v10 文档示例中的按钮禁用条件是 mutation.isLoading(TanStack Query v4 对 mutation 提供的标志位)。在 v11 相关文档的等价示例里,该写法已经演变为 mutation.isPending(TanStack Query v5 的语义),而 v10 时期的 isLoading 在 v5 中含义被重新定义为“首次加载且无数据”。做版本迁移时(可参考 migrate-from-v10-to-v11.mdx),要格外留意这类 React Query 语义变化。

三、底层探秘:trpc.xxx.useMutation 是怎么把代理解析成真 Hook 的

类型安全的魔法来自递归 Proxy。看 decorationProxy.ts 的实现:

return createRecursiveProxy(({ path, args }) => {
  const pathCopy = [...path];

  // The last arg is for instance `.useMutation` or `.useQuery()`
  const lastArg = pathCopy.pop()!;

  if (lastArg === 'useMutation') {
    return (hooks as any)lastArg;
  }
  // ...
});

当你写下 trpc.login.useMutation(opts) 时,实际发生的是:

  1. 代理把调用路径 ['login', 'useMutation'] 捕获下来;
  2. 弹出最后一个段 'useMutation',剩余部分 ['login'] 就是 procedure 路径;
  3. pathCopyopts 转发给内部真实的 useMutation(path, opts)

所以“路径是字符串还是元组”其实只是表面形式,最终都被归一化为 readonly string[]。这也解释了为什么 tRPC 能对任意深层嵌套路由做到「无样板、无字符串拼接、全推断」。

真正的 Hook 实现在 createHooksInternal.tsx,它内部调用了 TanStack Query 的 useMutation

function useMutation(
  path: readonly string[],
  opts?: UseTRPCMutationOptions<unknown, TError, unknown, unknown>,
): UseTRPCMutationResult<unknown, TError, unknown, unknown> {
  const { client, queryClient } = useContext();

  const mutationKey = getMutationKeyInternal(path);

  const defaultOpts = queryClient.defaultMutationOptions(
    queryClient.getMutationDefaults(mutationKey),
  );

  const hook = __useMutation(
    {
      ...opts,
      mutationKey: mutationKey,
      mutationFn: (input) => {
        return client.mutation(...getClientArgs([path, { input }], opts));
      },
      onSuccess(...args) {
        const originalFn = () =>
          opts?.onSuccess?.(...args) ?? defaultOpts?.onSuccess?.(...args);

        return mutationSuccessOverride({
          originalFn,
          queryClient,
          meta: opts?.meta ?? defaultOpts?.meta ?? {},
        });
      },
    },
    queryClient,
  ) as UseTRPCMutationResult<unknown, TError, unknown, unknown>;

  hook.trpc = useHookResult({
    path,
  });

  return hook;
}

这段源码印证了“薄封装”的定位,同时透露出 tRPC 在薄封装之上加的几层关键逻辑:

  • mutationKey 由路径生成getMutationKeyInternal(path) 保证每个 procedure 有稳定的 mutation key。这让你能基于路径配置 mutation 默认值(queryClient.setMutationDefaults),也让服务端返回后 tRPC 能把该 key 的缓存失效/刷新串起来。
  • mutationFn 调用的不是 fetch,而是 tRPC client 的 client.mutationgetClientArgs([path, { input }], opts) 会组装出「路径 + 已序列化输入 + headers/信号等选项」,走 tRPC 自身的传输层(HTTP 批量、错误规范化都在这一层完成)。
  • onSuccess 被包装成 mutationSuccessOverride:它把「用户传的 opts.onSuccess」「queryClient 上的 mutation 默认回调」以及通过 config.overrides.useMutation.onSuccess 注入的全局逻辑合并成一条执行链。默认实现只是透传调用 originalFn(),但这一扩展点正是 @trpc/react-query 提供给框架作者做“成功后自动失效查询”之类的钩子。
  • 返回对象额外挂载 hook.trpcuseHookResult({ path }) 记录当前 mutation 对应的路径,供调试与工具函数(如 useUtils())内部使用。

类型契约:MutationLike

如果你在写通用组件、封装表单或自建 HOC,需要“把某个 procedure 的 useMutation 当参数传入”时,仓库提供了对应的鸭子类型抽象 mutationLike.ts

export type MutationLike<TRoot, TProcedure> = {
  useMutation: (
    opts?: InferMutationOptions<TRoot, TProcedure>,
  ) => InferMutationResult<TRoot, TProcedure>;
};

配套的 InferMutationLikeInput / InferMutationLikeData 可以反推出任意 “MutationLike” 的输入与输出类型。这个 Polymorphism 设计(mutation/query/router 分别对应 queryLike.tsrouterLike.ts)是 v10 后期为支持“将整棵 router 对象整体传给子组件”而引入的,让类型系统在不丢失推断的前提下接受“形状匹配”的对象。

四、在业务中用好 mutation 的完整能力

虽然 v10 的 useMutation 文档只给了最小示例,但既然它“就是 TanStack Query mutation”,你就可以放心组合出以下生产级用法(类型均由 tRPC 保证):

1. mutatemutateAsync

  • mutate(input):fire-and-forget 风格,状态变化通过返回的 mutation 对象响应式呈现,组件里最常见;
  • mutateAsync(input):返回 Promise,适合在事件处理器里做“先写入、再跳转/刷新”等串行逻辑,配合 try/catch 捕获 TRPCClientError

2. 回调:onSuccess / onError / onSettled

const mutation = trpc.post.create.useMutation({
  onSuccess: () => {
    // 例如:toast 提示、表单重置
  },
  onError: (error) => {
    // error 是 TRPCClientError,可读 error.message / error.data.code
  },
  onSettled: () => {
    // 无论成败都会执行,适合关闭 loading 遮罩
  },
});

由于回调的 error 参数被推断为 TRPCClientErrorLike<AppRouter>onError 里直接访问 error.data.code(如 UNAUTHORIZEDCONFLICT)就是类型安全的,无需手动断言。

3. mutation 成功后失效并刷新相关查询

写操作之后最常见的诉求是“让相关列表重新拉取”。搭配 useUtils.mdx

const utils = trpc.useUtils();

const createPost = trpc.post.create.useMutation({
  onSuccess: () => {
    utils.post.list.invalidate();
  },
});

4. mutation 状态对象概览

一次 useMutation() 返回的对象包含(均由 TanStack Query v4 提供、tRPC 保持透传):

  • data:成功后服务端返回的强类型数据;
  • error:失败时的 TRPCClientError(无失败为 null);
  • variables:最近一次 mutate 传入的输入;
  • status'idle' | 'loading' | 'success' | 'error'
  • isIdle / isLoading / isSuccess / isError:与 status 对应的布尔快捷量(v10 语义);
  • mutate / mutateAsync:触发执行;
  • reset():把 mutation 状态清回 idle,常用于关闭错误弹窗后重置 UI。

五、类型推断:input 与 output 从哪来

mutationLike.ts 中可以看到两条核心推断工具:

  • inferProcedureInput:把 zod/任意 validator 的 schema 抽成入参类型,即 mutate(input) 的入参类型;
  • inferTransformedProcedureOutput:获取经过 data transformer(如 superjson)处理后的输出类型,即 data 的类型。

也就是说,服务端 login procedure 一旦改 schema(比如 name 变成 name: z.string().min(1),或返回值多一个字段),客户端 mutation.mutate({...})data.user 的用法会立刻在编译期报错——这正是 @trpc/react-query “Move Fast and Break Nothing” 的核心体验。关于 createTRPCReact 泛型如何从 setup.mdx 一路注入,以及手动从 Router 提取类型的技巧,可进一步阅读 infer-types.md

总结

useMutation 的设计哲学可以概括为一句话:mutation 的所有运行时能力交给 TanStack Query,所有端到端类型安全交给 tRPC 的 Proxy 与类型推断,两者通过 mutationKey = procedure path 这一桥梁结合

  • 服务端用 .mutation() + .input(zod) 定义写操作;
  • 客户端用 trpc.<path>.useMutation() 获得自带类型、状态机、错误对象与回调的完整 mutation;
  • 底层在 createHooksInternal.tsx 中把路径映射为 mutationKey,把执行体换成 client.mutation,把 onSuccess 汇入可扩展的 override 链;
  • 需要抽象/复用 mutation 形状时,使用 MutationLike 家族类型保持类型推断不丢失。

想深入对比 query 形态,推荐继续阅读同目录的 useQuery.md;想了解如何在 mutation 后做服务端数据再取回、序列化传输等细节,可分别参考 useUtils.mdxdata-transformers.md

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

项目优选

收起
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