TanStack Query Preact 适配层的UseMutateAsyncFunction类型:理解mutateAsync的返回类型与用法
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
本文围绕 TanStack Query 在 Preact 框架适配层(@tanstack/preact-query)公开的类型别名UseMutateAsyncFunction展开,讲解它在 useMutation 返回结果中的位置、四个泛型参数的含义、与UseMutateFunction(即mutate)的区别,以及为何返回 Promise 的行为会成为"并发提交表单、乐观更新回滚、等待全部 mutation 完成"等场景的类型基础。读完你既能按类型签名精确写出类型安全的 mutation 调用,也能理解mutateAsync从 Preact Hook 到 query-core 底层Mutation.execute的完整调用链。
UseMutateAsyncFunction类型别名速览
在 type-aliases 文档目录中,该类型的定义只有一行:
type UseMutateAsyncFunction<TData, TError, TVariables, TOnMutateResult> = MutateFunction<TData, TError, TVariables, TOnMutateResult>;也就是说,它并不是一个全新的函数形态,而是把@tanstack/query-core中的MutateFunction原样再导出一次,并改名为带有Use前缀的框架层类型,用来表达"这是由useMutation返回的mutateAsync"这一语义。其源码位置在 packages/preact-query/src/types.ts:453,紧随 UseMutateFunction 定义 之后,注释明确写道:"The type ofmutateAsync, as returned byuseMutation. Similar toUseMutateFunction, but returns a promise which can be awaited."(这是useMutation返回的mutateAsync的类型,与UseMutateFunction类似,但返回一个可以被await的 Promise。)
与相邻类型的关系
在 packages/preact-query/src/types.ts 中,这一族类型构成完整的层级:
UseMutateFunction<TData, TError, TVariables, TOnMutateResult>(第 432 行):mutate的类型,返回void,是"发射后不管"(fire-and-forget)的形态,错误通过 mutation 状态暴露,而不是抛出。UseMutateAsyncFunction<...>(第 453 行):mutateAsync的类型,返回Promise<TData>。UseBaseMutationResult<...>(第 470 行):使用Override把 query-core 的MutationObserverResult中的mutate收窄为框架层的UseMutateFunction,并追加mutateAsync: UseMutateAsyncFunction<...>字段(第 482-487 行)。UseMutationResult<...>(第 499 行):useMutation的最终返回值,等价于UseBaseMutationResult。
因此在使用useMutation返回值时,编译器能同时给出两种调用形态的完整签名:mutate形如(...args) => void,mutateAsync形如(...args) => Promise<TData>。
四个泛型参数逐一拆解
与UseMutationOptions、MutateFunction保持完全一致的顺序,UseMutateAsyncFunction携带四个类型参数:
| 类型参数 | 默认值 | 含义 |
|---|---|---|
TData | unknown | mutation 函数(mutationFn)成功 resolve 出来的数据类型。 |
TError | DefaultError | mutation 函数可能抛出的错误类型,query-core 中默认取DefaultError(通常是Error)。 |
TVariables | void | 调用mutateAsync时需要传入 mutation 函数的变量类型。 |
TOnMutateResult | unknown | onMutate返回值的类型,它会被传给onSuccess/onError/onSettled作为context(文档中称onMutateResult)参数——典型用途是承载乐观更新时保存的"回滚数据"。 |
这四个参数在 query-core 的 MutateFunction 定义 中共享同名语义;而 preact 的 UseMutationOptions 又把MutationObserverOptions(本身继承 MutationOptions)中的_defaulted内部字段剔除后原样暴露,因此四个泛型在"选项"与"结果函数"之间是严格一一对应、可以自动推导的。
关于TVariables与参数可选性
mutateAsync的第一个形参是否为必填,取决于TVariables是否包含undefined。这一定义来自 query-core 的辅助类型MutateFunctionRest(packages/query-core/src/types.ts:1179):
export type MutateFunctionRest<TData, TError, TVariables, TOnMutateResult> = undefined extends TVariables ? [variables?: TVariables, options?: MutateOptions<...>] : [variables: TVariables, options?: MutateOptions<...>]当TVariables缺省为void(而undefined可赋值给void)时,变量参数是可选的,即mutateAsync()可以不带参数调用;当TVariables被推导为如string、{ id: number }这类具体类型时,第一个参数变为必填。第二个参数始终可选,它对应 query-core 中 MutateOptions 结构——允许你在单次调用时传入局部的onSuccess/onError/onSettled回调。
mutateAsync与mutate:两种调用形态的本质区别
mutation 概念区分查询与写入:与查询不同,mutation 通常用于"创建/更新/删除数据或产生服务端副作用",这是 useMutation.ts 顶部注释对useMutation的定位说明。mutate与mutateAsync表面上功能相同,差异集中在错误处理与并发控制上:
- 错误传播方式不同。
mutate是 fire-and-forget 的:返回void,错误通过mutation.error状态、status或挂在 mutation 上的onError呈现,不会抛出到事件处理函数之外。而mutateAsync返回Promise<TData>,mutation 失败时 Promise 会 reject,因此必须配合try/catch、catch链或Promise.all处理。这正是源码注释 "errors are surfaced through the mutation result, not thrown"(错误通过 mutation 结果暴露而非抛出)描述的对比。 - 并发多次调用的归属不同。Hook 级回调会为每次 mutation 触发,但"单次调用传入的回调"只对最后一次调用生效。文档注释指出(useMutation.ts:128-129):"Callbacks passed per call to
mutateonly fire for the last call —mutateAsyncgives you a promise per call instead"。即:想要"一次性提交多条 todo 并分别等待"的场景,必须依赖mutateAsync逐调用返回的 Promise,Promise.all/Promise.allSettled才能拿到每一次调用的结果。 - 返回值可用于流程编排。
mutateAsync的成功值(TData)可以在.then或await之后继续参与后续逻辑,而mutate拿不到返回值。
源码层面的差异位于 useMutation.ts:224-244:
const mutate = useCallback<UseMutateFunction<TData, TError, TVariables, TOnMutateResult>>( (...args) => { observer.mutate(args[0] as TVariables, args[1]).catch(noop) // 错误被吞掉 }, [observer], ) ... return { ...result, mutate, mutateAsync: result.mutate }注意最后一行:框架层没有单独实现mutateAsync,它直接复用了MutationObserver.mutate并原样挂到返回值上,而类型上则同时收窄了mutate、补充了mutateAsync签名。因此 Promise 语义的源头在 query-core:MutationObserver.mutate(packages/query-core/src/mutationObserver.ts:136-151)先记录本次调用的#mutateOptions、从MutationCache构建 mutation,然后直接return this.#currentMutation.execute(variables)——把内部执行 Promise 透传出来。
底层调用链:从 Promise 到 mutation 执行
MutateFunction的签名是(...rest) => Promise<TData>,这个 Promise 的完整生命周期可以沿 query-core 追踪到 Mutation.execute:
- 构造
MutationFunctionContext(含client、meta、mutationKey); - 通过
createRetryer包装实际mutationFn(variables, mutationFnContext),并应用retry、retryDelay、networkMode配置; - 进入
pending状态后依次触发:mutationCache.config.onMutate→ 选项级options.onMutate,其返回值成为context(即TOnMutateResult); - 等待
retryer.start()resolve 后,依次触发 cache 级与选项级的onSuccess、onSettled,随后#dispatch({ type: 'success', data })并return data; - 任一环节抛错则进入
catch分支,触发onError与onSettled,最终把错误作为 rejection 抛出。
因此await mutation.mutateAsync(...)得到的TData,正是mutationFn的 resolve 值;onMutate返回的 context 会在整个 Promise 生命周期内作为第 3 个参数传给onError等回调,用于乐观更新回滚。
实战用法示例
场景一:等待全部 mutation 完成(批量提交)
当你想"点一次按钮同时新增多条数据,并等待它们全部落地"时,per-call 回调只对最后一次mutate生效,正确的做法是用mutateAsync+Promise.all(示例源自 useMutation.ts 文档注释):
import { useMutation, useQueryClient } from '@tanstack/preact-query' function AddTodos() { const queryClient = useQueryClient() const addMutation = useMutation({ mutationFn: addTodo, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) async function handleAddAll(todos: Array<string>) { try { await Promise.all(todos.map((todo) => addMutation.mutateAsync(todo))) } catch (error) { console.error('Failed to add todos:', error) } } return ( <button onClick={() => handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])}> Add all </button> ) }如果这些子 mutation 可能各自独立失败、而你又想知道"具体是哪几条失败",则换用Promise.allSettled逐个检查addResult.status === 'rejected'(完整示例见 useMutation.ts:172-190)。
场景二:乐观更新回滚
TOnMutateResult最常见的承载对象就是回滚数据。在onMutate中先cancelQueries并保存旧数据,失败时用其恢复(见 useMutation.ts:100-119 的 JSDoc 示例,以及 乐观更新指南):
const addMutation = useMutation({ mutationFn: addTodo, onMutate: async (newTodo) => { await queryClient.cancelQueries({ queryKey: ['todos'] }) const previousTodos = queryClient.getQueryData<Array<string>>(['todos']) queryClient.setQueryData<Array<string>>(['todos'], (old) => [ ...(old ?? []), newTodo, ]) // 传给 onError 作为 context:类型上正是 TOnMutateResult return { previousTodos } }, onError: (_err, _newTodo, context) => { queryClient.setQueryData(['todos'], context?.previousTodos) }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })此时useMutation的四个泛型可被自动推导:TData来自addTodo的返回类型、TVariables来自其入参、TOnMutateResult来自onMutate的返回对象形状,mutateAsync的参数与返回类型都会随之收紧,await之后拿到的数据类型精确无误。
类型层面的验证与推导
框架自带类型测试可以印证UseMutateAsyncFunction的行为约定,见 packages/preact-query/src/tests/useMutation.test-d.tsx:
it('should type mutateAsync with correct return type', () => { // mutation.mutateAsync 可调用、返回 Promise<TData> expectTypeOf(mutation.mutateAsync).toBeCallableWith('test') expectTypeOf(mutation.mutateAsync('test')).toEqualTypeOf<Promise<number>>() }) // TVariables 为 void 时,mutateAsync 可不带参数调用 expectTypeOf(mutation.mutateAsync).toBeCallableWith()这与前文MutateFunctionRest的可选参数规则互相印证:当TVariables不含undefined时必须传参,返回类型恒为Promise<TData>。
进一步,若希望把 mutation 的选项集中管理并在useMutation之外复用,可借助 mutationOptions(实现见 packages/preact-query/src/mutationOptions.ts),它与useMutation接受同一组UseMutationOptions,便于通过mutationKey配合 useMutationState 查询 mutation 状态。
小结
UseMutateAsyncFunction<TData, TError, TVariables, TOnMutateResult>本质是 query-coreMutateFunction的框架层再导出,描述useMutation返回的mutateAsync,返回类型恒为Promise<TData>;- 它与返回
void的UseMutateFunction(mutate)构成互补:前者可用于await/Promise.all编排与错误捕获,后者适合事件回调里"发射后不管"的写法; - 四个泛型参数与
UseMutationOptions一一对应,TVariables是否包含undefined决定变量参数是否必填; - Promise 语义的底层实现在 MutationObserver.mutate 与 Mutation.execute,框架层在 useMutation.ts:244 直接复用
result.mutate作为mutateAsync。
理解了这一类型别名,你就掌握了 TanStack Query mutation API 中"可等待调用"一侧的完整类型契约:无论是批量写入、逐步上传、乐观回滚还是提交后跳转导航(可结合 mutations 指南与 invalidations-from-mutations 指南进一步阅读),写出的代码都能获得端到端的类型安全。
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考