ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Preact Query 的 UseMutationOptions 接口全解析:从泛型约束到乐观更新实战

2026/9/10 1:32:11 拓冰建站 浏览量
Preact Query 的 UseMutationOptions 接口全解析:从泛型约束到乐观更新实战 Preact Query 的 UseMutationOptions 接口全解析从泛型约束到乐观更新实战【免费下载链接】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/queryUseMutationOptions是tanstack/preact-query中useMutation唯一接收的配置对象类型。它完整继承了tanstack/query-core的MutationObserverOptions仅剔除内部_defaulted标记通过四个泛型参数TData/TError/TVariables/TOnMutateResult将 mutationFn 的返回类型、错误类型、入参类型以及乐观更新回滚数据在编译期串联起来。本文以该接口为线索结合仓库源码讲解每个选项字段的语义、泛型的推导规则并给出可直接运行的 Preact 组件示例。该接口定义于 packages/preact-query/src/types.ts:411是useMutation的入口类型契约。它的所有具体配置字段都来自 query-core 的MutationObserverOptions与MutationOptions见 packages/query-core/src/types.ts:1103因此理解这份接口就等于理解了 Preact Query 一次变更mutation从配置、派发、执行到回调的全部行为。接口定位useMutation 的选项协议在 Preact Query 中mutation与 query 的角色完全不同query 用于读取服务端数据而 mutation 通常用于创建、更新、删除数据或执行服务端副作用如登录、点赞、上传。useMutation是承载这一能力的 Hook它的完整函数签名在 useMutation 参考文档 中function useMutationTData, TError, TVariables, TOnMutateResult( options: UseMutationOptionsTData, TError, TVariables, TOnMutateResult, queryClient?: QueryClient, ): UseMutationResultTData, TError, TVariables, TOnMutateResult可见UseMutationOptions直接决定了useMutation的类型推导结果。它的官方定义如下类型interface UseMutationOptionsTData, TError, TVariables, TOnMutateResult继承OmitKeyofMutationObserverOptionsTData, TError, TVariables, TOnMutateResult, _defaulted也就是说它是 query-core 中MutationObserverOptions的全集唯一区别是通过OmitKeyof工具类型抹掉了内部的_defaulted布尔标记——该标记仅供 query-core 内部在配置默认化defaulting时使用对调用方既无意义也不应被暴露因此接口层将其剥除。这一点与同目录下 UseQueryOptions 等接口的设计思路一致框架层 API 永远面向开发者消费内部实现细节留在 query-core。在 packages/preact-query/src/types.ts:396-419 中还紧邻定义了两个相关工具类型AnyUseMutationOptions将四个泛型参数全部放宽为any适合编写不关心具体类型的辅助函数例如需要接收任意 mutation 的配置的通用封装。UseMutateFunction/UseMutateAsyncFunction描述mutate/mutateAsync的函数签名会把配置中声明的TVariables等类型透传到调用处。四个泛型参数详解UseMutationOptions的四个泛型参数构成了 mutation 的类型闭环从入参到成功返回值再到可能的错误与乐观更新上下文全部可被 TypeScript 静态推导。泛型参数默认值含义影响面TDataunknownmutationFn 成功 resolve 后的数据类型useMutation().data、onSuccess的data参数TErrorDefaultErrormutationFn 可能抛出的错误类型error、isError、onError/onSettled的错误参数、throwOnErrorTVariablesvoid传给mutate/mutateAsync的变量类型mutate(variables)的入参、mutationFn的第一参数、各回调的variablesTOnMutateResultunknownonMutate的返回值类型作为onMutateResult传给onSuccess/onError/onSettled乐观更新时的回滚快照数据TDatamutation 函数解析出的类型当你在useMutation中传入一个返回PromiseT的mutationFn时TypeScript 会自动把TData推导为T。随后 Hook 返回结果的data字段MutationObserverBaseResult.data在成功状态下即为此类型。在 Preact 组件里通常这样消费const createPost useMutation({ mutationFn: (title: string) api.createPost(title), // PromisePost }) // createPost.data 的类型为 Post | undefined if (createPost.isSuccess) { console.log(createPost.data.id) }TErrormutation 函数可能抛出的错误类型TError默认使用 query-core 导出的DefaultError框架默认错误类型。它同时约束了失败状态下的error字段以及onError、onSettled回调中错误参数的签名。若你使用 axios 之类的库并希望错误信息携带服务端返回结构可显式收紧interface ApiError { code: number message: string } const mutation useMutationPost, ApiError, string({ mutationFn: (title) createPost(title), onError: (error) { // error 被推导为 ApiError可直接读取 error.code }, })TVariables传给 mutate / mutateAsync 的变量TVariables默认是void。它的值很关键query-core 会依据TVariables 是否允许 undefined来切换mutate的参数是否为可选。从 packages/query-core/src/types.ts:1179-1201 的MutateFunctionRest可以看到type MutateFunctionRestTData, TError, TVariables, TOnMutateResult undefined extends TVariables ? [variables?: TVariables, options?: MutateOptions...] // 可选参数 : [variables: TVariables, options?: MutateOptions...] // 必选参数因此不带变量的 mutation如mutationFn: () logout()TVariables推导为void调用时mutate()可以不带参数。带变量的 mutation如mutationFn: (id: number) deleteTodo(id)TVariables被推导为numberTypeScript 会强制你传入mutate(123)。若你在类型层面手动写成useMutationPost, Error, void则mutate的第一个参数会变成可选的——这正是undefined extends void条件命中void允许undefined时的行为。TOnMutateResult为乐观更新而生的上下文类型TOnMutateResult是四个泛型中最具业务味道的一个。它表示onMutate的返回值并被透传给onSuccess/onError/onSettled的第三个参数onMutateResult典型用途就是承载乐观更新所需的回滚快照。官方源码注释对此的表述是The type returned byonMutate, passed toonSuccess/onError/onSettledas theironMutateResultparameter — useful for optimistic-update rollback data.下面的经典乐观更新 失败回滚示例摘自 useMutation 参考文档最能说明它的作用import { useMutation, useQueryClient } from tanstack/preact-query function AddTodo() { const queryClient useQueryClient() const addMutation useMutation({ mutationFn: (newTodo: string) addTodo(newTodo), onMutate: async (newTodo) { // 1. 取消进行中的相关查询避免旧数据覆盖乐观更新 await queryClient.cancelQueries({ queryKey: [todos] }) // 2. 保存当前快照作为回滚依据 const previousTodos queryClient.getQueryDataArraystring([todos]) // 3. 立即把新数据写进缓存乐观更新 queryClient.setQueryDataArraystring([todos], (old) [ ...(old ?? []), newTodo, ]) // 4. 返回快照类型即 TOnMutateResult return { previousTodos } }, onError: (_err, _newTodo, onMutateResult) { // 失败时用快照恢复原状onMutateResult 被推导为 { previousTodos?: ... } queryClient.setQueryData([todos], onMutateResult?.previousTodos) }, onSettled: () { // 5. 收尾无论成败都重新拉取最新数据 queryClient.invalidateQueries({ queryKey: [todos] }) }, }) return button onClick{() addMutation.mutate(Item)}Add/button }由于TOnMutateResult会同时约束onMutate的返回值与后续三个回调拿到的onMutateResult参数编译器能保证你return什么onError里就能安全?.什么这是乐观更新代码不易出错的类型基础。仓库内完整示例可参考 examples/preact/simple/src 等 Preact 示例目录其余框架React/Solid/Vue中相同模式见 docs/framework/preact/guides/mutations.md 指南及 invalidations-from-mutations。全部选项字段从 MutationOptions 与 MutationObserverOptions 继承而来UseMutationOptions本身不声明任何新字段所有可用配置项都来自 query-core 的继承链MutationObserverOptions // throwOnError └── MutationOptions // mutationFn/mutationKey/回调/retry/networkMode/gcTime/meta/scope ... └──内部标记 _defaulted被 OmitKeyof 剔除两个关键接口的定义分别位于 packages/query-core/src/types.ts:1103-1141MutationOptions与 packages/query-core/src/types.ts:1143-1150MutationObserverOptions。整理后的完整字段清单如下字段类型默认行为说明mutationFn(variables: TVariables, context) PromiseTData \| TData必填否则调用 mutate 会抛错执行服务端副作用的异步函数第一参数即TVariablesmutationKeyMutationKey无可选标识可用于useMutationState、useIsMutating按 key 过滤查找该 mutationonMutate(variables, context) PromiseTOnMutateResult \| TOnMutateResult无在 mutation 执行前同步触发通常做缓存乐观写入并返回回滚快照onSuccess(data, variables, onMutateResult, context) ...无成功后触发可同步/异步onError(error, variables, onMutateResult, context) ...无失败后触发onSettled(data, error, variables, onMutateResult, context) ...无无论成功失败都会在最后触发常用作失效查询retryboolean \| number \| (failureCount, error) booleanquery-client 默认值通常为 3失败重试策略mutation 默认继承全局配置retryDelaynumber \| (retryAttempt, error) number全局默认每次重试的等待时间networkModeonline \| always \| offlineFirst继承全局离线状态下 mutation 如何处理如保存至恢复联网后执行gcTimenumber全局默认mutation 缓存留存时间毫秒throwOnErrorboolean \| (error: TError) booleanfalseMutationObserverOptions新增字段设为true时mutateAsync会把错误向上抛出供 try/catch 捕获metaRecordstring, unknown无附加元数据可在 MutationCache 事件与回调的context.meta中读取scopeMutationScope无将 mutation 关联到指定作用域配合MutationCache的按 scope 操作_defaulted——query-core 内部标记已被OmitKeyof从UseMutationOptions中剔除不要也不允许传入从 mutationObserver.ts:44-93 的实现可以看出observer 在setOptions时会对比新旧mutationKey经hashKey哈希后比较一旦 key 改变就reset()当前状态否则若正处于pending则仅更新执行中的 mutation 选项。这解释了为什么中途修改mutationFn不会打断已经在跑的任务而修改mutationKey会重置状态。throwOnError 与回调组合的语义边界throwOnError与回调的组合值得留意定义见 packages/query-core/src/types.ts:1149onError负责声明式的错误处理任何使用该 mutation 的调用都会触发throwOnError: true让mutateAsync返回的 Promise 变为 rejected便于在调用点用try/catch精确控制错误流但并不会阻止onError执行更精细的做法是传入函数throwOnError: (error) error.code 401只在特定错误上抛出。retry / networkMode / gcTime为什么 mutation 也关心缓存策略很多人误以为 mutation 是一次性动作、与缓存无关。实际上每个 mutation 都由 MutationCache 托管mutationKeygcTime决定了其状态在内存中的驻留时长retry/networkMode则控制失败重试与离线行为。这些选项默认继承自QueryClient的全局默认值若你在new QueryClient()时配置了defaultOptions.mutations这里不传就会使用全局配置——这正是UseMutationOptions继承MutationOptions中这些字段的原因。useMutation 如何消费这些选项源码调用链理解UseMutationOptions之后再看 packages/preact-query/src/useMutation.ts:192-245 中useMutation的实现就能把类型契约与运行时行为对应起来创建 observer通过useState惰性初始化一个MutationObservernew MutationObserver(client, options)整个生命周期只创建一次同步选项在useEffect中调用observer.setOptions(options)保证每次渲染传入的新选项都会更新到同一个 observer选项对象变化是随渲染频繁发生的绝不重建 observer订阅结果用 Preact 内置的useSyncExternalStore订阅 observer并利用notifyManager.batchCalls做批处理避免一次 mutation 状态变迁触发多次无谓渲染派发入口mutate/mutateAsync最终落到 observer 的mutate方法——见 packages/query-core/src/mutationObserver.ts:136-151它把本次调用携带的 per-call 回调MutateOptions暂存然后通过client.getMutationCache().build(client, options)从缓存中构建或复用一个MutationaddObserver后调用execute(variables)。也就是说你传入UseMutationOptions里的mutationFn、mutationKey、重试与回调字段决定了MutationCache如何构建并执行一次变更而配置里没有出现的每次调用差异如某个按钮点击后额外导航则应放在mutate的第二个参数MutateOptions上——这是接口划分的隐藏智慧。Hook 级回调与 per-call 回调的区别useMutation的返回文档对此有明确说明见 useMutation.ts:29-33Hook 级回调写在UseMutationOptions里的onSuccess/onError/onSettled对每一次 mutation 都会触发per-call 回调mutate(variables, { onSuccess, onError, onSettled })只对最近一次调用触发且组件卸载订阅被移除后即使 mutation 才 settle 也不会再触发。因此想触发调用点本地副作用如成功后跳转、失败后 toast 提示时应使用 per-call 回调想集中处理每一次变更后的缓存同步则应放在UseMutationOptions层。mutateAsync 与并行场景Hook 级回调在并行多次 mutate 时只能反映最后一次结果若你需要逐次等待每个结果应改用mutateAsync返回PromiseTDataasync function handleAddAll(todos: string[]) { try { await Promise.all(todos.map((todo) addMutation.mutateAsync(todo))) } catch (error) { console.error(Failed to add todos:, error) } }若这些变更可能独立失败而你希望保留哪些成功、哪些失败的明细信息则用Promise.allSettled替代Promise.all逐个检查addResult.status。两种模式在 useMutation 参考文档 中均有完整示例。与 mutationOptions() 协同把类型配置变成可复用对象UseMutationOptions与 mutationOptions 参考文档 中导出的mutationOptions()工厂函数是同一份配置的两种使用形态import { mutationOptions, useMutation, useMutationState } from tanstack/preact-query const createPostOptions mutationOptions({ mutationKey: [posts], mutationFn: (title: string) createPost(title), onSuccess: () queryClient.invalidateQueries({ queryKey: [posts] }), }) // 直接复用同一份 options const mutation useMutation(createPostOptions) // 或通过 mutationKey 在别处观察它的状态 const state useMutationState({ filters: { mutationKey: [posts] }, })mutationOptions()与useMutation接收的类型完全一致即UseMutationOptions但它不依赖 Hook 上下文因此可以定义在组件外部、被多个调用点共享或用于useMutationState/useIsMutating见 useIsMutating 参考文档按mutationKey跨组件观察。这是对配置即数据思想的实践让类型、行为与状态观察共用同一份声明。实战一个状态完整的 Preact mutation 组件综合前面所有内容下面是一个渲染 mutation 自身状态的完整组件沿用 useMutation 参考文档 的模式import { useMutation } from tanstack/preact-query function AddTodo() { const addMutation useMutation({ mutationKey: [todos], mutationFn: (title: string) addTodo(title), onSuccess: () console.log(saved), }) return ( div {addMutation.isPending ? ( Adding todo... ) : ( {addMutation.isError ? ( divAn error occurred: {addMutation.error.message}/div ) : null} button onClick{() addMutation.mutate(Item)}Add/button / )} /div ) }组件通过statusidle | pending | error | success派生的isPending/isError等布尔量驱动 UI无需手写状态机——这些布尔量由 query-core/src/mutationObserver.ts:153-167 的#updateResult从 mutation 状态统一计算并随订阅广播。这份体验正是UseMutationOptions在类型层打下的地基把你要做什么mutationFn与你关心什么回调、状态声明清楚运行时就由统一的 observer 机制接管。使用建议小结类型推导优先显式标注兜底尽量让 TS 从mutationFn反推TData/TError/TVariables仅当无法推断如mutationFn来自弱类型 SDK时才显式写明四个泛型。把回滚快照交给 TOnMutateResult乐观更新时在onMutate中return { snapshot }onError通过onMutateResult参数还原全程类型安全。配置共享用 mutationOptions()跨组件复用的配置提取到组件外结合mutationKey可用useMutationState/useIsMutating观察可参考 mutationOptions 文档。不要传_defaulted它是 query-core 内部标记UseMutationOptions已显式排除传入会被 TypeScript 拒绝这恰是接口设计严谨性的体现。框架差异最小化由于配置全部收敛在 query-core 的MutationObserverOptionsPreact Query 的这份接口与 React/Solid/Vue 版本在字段语义上保持一致学习成本可跨框架复用。延伸阅读useMutationPreact参考本接口的唯一消费方含乐观更新、批量提交等完整示例mutationOptionsPreact参考UseMutationOptions的组件外复用形态Mutations 指南 与 Mutations 导致的失效接口背后的业务模式类型定义、query-core 选项基类、MutationObserver 实现源码级依据useMutationState、useIsMutating按 mutationKey 观察 mutation 状态的配套 HookMutationCache 参考mutation 在缓存层中的生命周期管理【免费下载链接】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),仅供参考