首页
/ Preact Query 实战:Mutation 成功后如何自动失效并重取相关查询(Invalidations from Mutations)

Preact Query 实战:Mutation 成功后如何自动失效并重取相关查询(Invalidations from Mutations)

2026-09-08 13:40:23作者:曹令琨Iris

本指南面向使用 @tanstack/preact-query 的开发者,讲解为什么 Mutation 成功之后通常需要联动失效(invalidate)相关查询,并给出在 useMutation 回调中调用 QueryClient.invalidateQueries 的标准写法与多查询失效策略。读完你将掌握 Mutation 生命周期回调的接线方式、onSuccess 中返回 Promise 的时序语义,以及 invalidateQueries 从"标记失效"到"自动重取"的底层实现原理。

在 Preact Query(本仓库中对应 packages/preact-query)中,"查询(Query)负责读、Mutation 负责写"是服务端状态管理的核心分工。写操作成功后,应用往往需要让"依赖同一份数据"的查询感知到变化,这正是本指南要解决的核心问题。

为什么 Mutation 成功后通常要失效查询

失效查询只是解决问题的"一半",另一半是弄清楚应该在什么时候去失效。经验法则是:当应用中的某个 Mutation 成功执行后,极大概率存在与之相关的查询需要被失效并重取,以反映 Mutation 带来的最新数据变化。

例如,假设我们有一条"新增 todo"的 Mutation:

const mutation = useMutation({ mutationFn: postTodo })

当一个 postTodo Mutation 成功之后,我们通常希望所有 todos 相关的查询都被失效并可能重取,以便列表中出现新建的 todo。为此,可以在 useMutationonSuccess 选项中调用 QueryClientinvalidateQueries 方法(Preact 示例中导入自 @tanstack/preact-query):

import { useMutation, useQueryClient } from '@tanstack/preact-query'

const queryClient = useQueryClient()

// 当这个 mutation 成功时,失效任何 queryKey 前缀为 `todos` 或 `reminders` 的查询
const mutation = useMutation({
  mutationFn: addTodo,
  onSuccess: async () => {
    // 如果要失效单个查询
    await queryClient.invalidateQueries({ queryKey: ['todos'] })

    // 如果要失效多个查询
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['todos'] }),
      queryClient.invalidateQueries({ queryKey: ['reminders'] }),
    ])
  },
})

其中 useQueryClient 读取的是组件树中 QueryClientProvider 提供的客户端实例。Preact 适配层在 QueryClientProvider.tsx 中通过 createContext + useContext 实现:若没有传入显式的 queryClient 参数且组件树上不存在 Provider,会直接抛出 No QueryClient set, use QueryClientProvider to set one。因此上述代码中的 queryClient 一定来自某个 QueryClientProvider client={queryClient} 包裹的祖先组件。

触发失效的三种接入点:onSuccess / onError / onSettled

useMutation 的所有生命周期回调都可以用来接线失效逻辑(详见 useMutation 的实现mutations 指南):

回调 触发时机 典型用途
onMutate(variables, context) Mutation 即将开始(mutationFn 执行前) 取消进行中的查询、写入乐观数据
onSuccess(data, variables, context) Mutation 成功 失效查询、写入真实数据(setQueryData
onError(error, variables, context) Mutation 失败 回滚乐观更新、清理数据
onSettled(data, error, variables, context) 无论成功还是失败,都必定触发 收尾性的失效/重取

其中 onErroronSettled 在"写失败后仍需要刷新缓存"(例如别的客户端可能已经改了数据)时非常有用;而 onSettled 因为成功失败都会执行,适合放置"无论如何都重新同步一次列表"的逻辑。关于用 onMutate/onError/onSettled 组合实现"先乐观更新、失败回滚、最终失效"的完整写法,可参考 optimistic-updates.md,其核心骨架如下:

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,
    ])
    return { previousTodos } // 传给 onError 用于回滚
  },
  onError: (_err, _newTodo, onMutateResult) => {
    queryClient.setQueryData(['todos'], onMutateResult?.previousTodos)
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

onSuccess 返回 Promise 的时序语义

重要:在 onSuccess 中返回一个 Promise(即写成 async 函数并 await 内部的 invalidateQueries),可以确保相关数据在 Mutation 完全结束之前完成更新——也就是说,在 onSuccess 被 fulfill 之前,Mutation 的 isPending 一直为 true

这一行为并非框架巧合,而是有明确的源码保证。在 query-core 的 mutation.ts 中,成功路径的执行顺序是:

  1. 执行 mutationCache.config.onSuccess(可选);
  2. await this.options.onSuccess?.(...)——Mutation 选项上的 onSuccess 会被真正 await
  3. await this.options.onSettled?.(...)
  4. 最后才 this.#dispatch({ type: 'success', data }),把 Mutation 自身状态推进为 success

也就是说,dispatch('success') 被有意安排在 onSuccess/onSettled 全部 resolve 之后。所以只要 onSuccessawait queryClient.invalidateQueries(...),UI 上就会呈现"按钮保持 pending,直到列表重取完成",从而避免"数据还没刷新就显示完成"的闪烁。

此外,调用 mutate 时还可以传入仅对本次调用生效的额外回调:

mutate(todo, {
  onSuccess: (data, variables, context) => {
    // 本次调用专属的成功处理(如关闭弹窗、跳转),在 useMutation 级 onSuccess 之后触发
  },
})

注意这些"调用级回调"只在对应那次调用期间且组件仍挂载时触发;若连续多次 mutate,钩子级回调每次都会执行,而调用级回调仅对最后一次调用触发。若希望每次调用都能拿到独立的 Promise,请改用 mutateAsync

invalidateQueries 底层原理:先标记失效,再按需重取

invalidateQueries 的语义在 query-core 中实现得非常清晰,位于 queryClient.ts

invalidateQueries(filters, options) {
  return notifyManager.batch(() => {
    this.#queryCache.findAll(filters).forEach((query) => {
      query.invalidate()
    })

    if (filters?.refetchType === 'none') {
      return Promise.resolve()
    }
    return this.refetchQueries({
      ...filters,
      type: filters?.refetchType ?? filters?.type ?? 'active',
    }, options)
  })
}

整个过程分三步:

  1. 批量匹配:整个操作被包在 notifyManager.batch 中(见 queryClient.ts),所有匹配/失效/重取通知会被合并在一次批处理里分发,避免多次触发渲染。
  2. 逐个标记失效:对每个匹配查询调用 query.invalidate()。在 query.ts 中,该方法在 state.isInvalidated 尚未置位时派发 { type: 'invalidate' } 动作,把查询标记为"已失效(invalidated)"。
  3. 按需重取:默认会继续调用 refetchQueries 立即重取。默认的重取范围是active(有活动观察者的查询),除非显式指定 typerefetchType。未被立即重取的非活动查询会带着失效标记保留,待其再次成为活动查询、窗口重新聚焦或满足 stale 条件时再重取。

refetchQueries 本身(见 queryClient.ts)默认 cancelRefetch: true,即若查询正在请求中会先取消再重取;同时会跳过 isDisabled()isStatic() 的查询,并把处于 paused(如离线)的请求直接视为完成。

过滤参数与重取控制详解

invalidateQueries 的第一个参数是 InvalidateQueryFilters(继承自 QueryFilters),第二个参数是 InvalidateOptions。这两组类型在 query-core 源码中有准确定义:

  • utils.ts 定义 QueryFilters
  • types.ts 定义 InvalidateQueryFilters extends QueryFiltersInvalidateOptions extends RefetchOptions

核心字段说明如下:

字段 类型 默认 作用
queryKey TQueryKey | 前缀元组 按 queryKey 前缀匹配查询;['todos'] 会命中 ['todos']['todos', 1]['todos', 'list'] 等以它为前缀的键
exact boolean false true 时要求 queryKey 完全相等才命中(partialMatchKey 前缀匹配失效)
type 'all' | 'active' | 'inactive' 参与"立即重取"的查询范围(active = 当前有活跃观察者)
refetchType 'all' | 'active' | 'inactive' | 'none' type ?? 'active' 单独控制"重取哪一类";'none' 表示只标记失效、不立即重取
predicate (query) => boolean 自定义匹配函数,无法用 queryKey 表达的场景使用
stale boolean 只包含/排除 stale 查询
fetchStatus 'fetching' | 'paused' | 'idle' 按当前抓取状态过滤

而第二个参数 InvalidateOptions extends RefetchOptions 支持:

  • cancelRefetch?: boolean——true(默认)先取消正在进行的请求再重取;false 则在已有请求运行时跳过重取;
  • throwOnError?: boolean——重取失败时是否抛出错误(默认静默吞掉)。

因此,如果你只想"让数据过期"而把重取推迟到查询再次被观察时执行,可以写:

await queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'none', // 只失效,不立即重取
})

要精确地只失效某一条 todo 的详情查询,则可配合 exact: true

await queryClient.invalidateQueries({
  queryKey: ['todo', todoId],
  exact: true,
})

从"失效一条"到"失效一片":前缀匹配与批量失效

queryKey 默认按前缀匹配意味着:一次 invalidateQueries({ queryKey: ['todos'] }) 就能让"所有以 ['todos'] 开头的查询"(列表、详情、分页、无限滚动)统一失效——这正是"新增一条 todo 后整片列表都应刷新"这一需求的精确映射。相关键规则见 query-keys.md

当一次写操作波及多组独立数据(例如新增 todo 同时会改变提醒数量)时,可按需逐组失效,也可以并行执行:

onSuccess: async () => {
  await Promise.all([
    queryClient.invalidateQueries({ queryKey: ['todos'] }),
    queryClient.invalidateQueries({ queryKey: ['reminders'] }),
  ])
},

由于 invalidateQueries 内部通过 notifyManager.batch 合并通知,批量调用不会带来额外的渲染放大;而逐组 await 保证每组重取都完成后再推进 Mutation 生命周期(配合上文"dispatch success 在回调之后"的时序,组件能把控加载状态)。

与写入缓存(setQueryData)方案的取舍

失效重取并非 Mutation 后同步数据的唯一手段。官方还提供了另一条路径:直接用 Mutation 返回的数据更新缓存(queryClient.setQueryData),即 updates-from-mutation-responses.md 讲解的"基于 Mutation 响应更新缓存"。

两者的适用场景差异可以概括为:

  • 失效重取(本指南):Mutation 返回体往往只是"服务端确认",无法简单映射到列表里的每一处(排序、分页、聚合都可能变化)。此时信任服务端、以 invalidateQueries 触发一次干净的重取是更稳妥的选择,代价是额外一次网络请求。
  • 直接写缓存(setQueryData):Mutation 返回的就是目标实体的完整新数据,且你能低成本地把它合并进现有列表。此时写缓存可省去重取,体验更即时,但要求你对缓存结构与服务端返回有强约定。

在实践中两者常结合使用:小对象用 setQueryData 即时落位,大范围列表用 invalidateQueries 兜底。由于失效发生在 Mutation 成功回调内,写缓存与失效的编排都集中在同一处 Mutation 定义里,逻辑可读性较高。

一份可运行的完整示例

把上述要点组合起来,一个典型"新增 Todo"场景在 Preact 中长这样(注意 onSuccessasync + await,确保 Mutation 的 isPending 覆盖到重取完成):

import { useMutation, useQueryClient } from '@tanstack/preact-query'

function AddTodo() {
  const queryClient = useQueryClient()

  const addMutation = useMutation({
    mutationFn: addTodo,
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
    onError: (error) => {
      console.error('添加失败:', error)
    },
  })

  return (
    <div>
      {addMutation.isPending ? (
        '正在添加…'
      ) : addMutation.isError ? (
        <span>出错了:{addMutation.error.message}</span>
      ) : null}
      <button
        onClick={() => addMutation.mutate('新任务')}
        disabled={addMutation.isPending}
      >
        添加 Todo
      </button>
    </div>
  )
}

更进一步

  • 若想深入理解 onSuccess/onError/onSettled 的完整签名与"连续 Mutation 只触发一次调用级回调"等边界行为,阅读 mutations.md
  • 本指南聚焦"Mutation 成功之后失效",而失效机制的完整(含失效过滤器、refetchType、后台重取差异)见 query-invalidation.md
  • 相关 API 完整定义见 QueryClient 参考文档invalidateQueriesrefetchQueriessetQueryData 等条目。
  • 想研究底层实现,可对照阅读 queryClient.tsquery.tsmutation.ts,并参考 useMutation.ts 中内置的失效示例注释。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391