你可能不需要 TanStack Query

2 阅读6分钟

在中后台管理系统场景下,最近在探索 Vue 3 项目前端架构演进时,在考虑一个问题:要不要引入 TanStack Query(以下简称 TQ)?

网上铺天盖地的推荐、官网炫目的例子、社区的“现代数据获取标配”标签,让人感觉不上 TQ 就落伍了。但经过一轮又一轮的推敲,我们得出了一个和主流声音不太一样的结论:在典型的中后台业务里,你可能真的不需要它。

甚至,把它的 Cache 能力直接引入,反而会破坏架构的纯粹性。

一、 TQ 确实有闪光点

它的 loading / isPending / isFetching 等声明式状态机制非常优雅,几乎消灭了手动管理 loading 变量的模板代码。自动去重(Deduplication)、防抖、后台静默刷新等特性,在高交互、频繁切换的消费级应用(如 Twitter、在线协作文档)里确实很香。

但这些优点主要服务于“重交互、实时性强”的场景。而中后台 90% 的页面是 CRUD + 列表/表单,核心流程是:打开列表 → 编辑 → 返回并刷新。在这种“拉取-显示-操作”的线性生命周期里,TQ 解决的问题往往不是我们的痛点,反而引入了新的麻烦。

二、 核心矛盾:缓存究竟该归谁管?

这是我们拒绝 TQ 最根本的架构理由。我们需要明确两个原则:

1. 缓存层应该是尽可能少的

在复杂的工程中,数据流经的层级越多,维护成本呈几何倍数增长。一旦你在 Service/Api 层已经有了状态缓存,再引入 TQ 缓存就会形成双缓存地狱:两个系统同时接管同一份数据。

当数据不一致或更新失效时,你很难分清是 Service 层的处理滞后,还是 TQ 的 staleTime 没设对。这种排查过程极度痛苦,本质上是架构层面的职责重叠。

2. 缓存属于“底层基础设施”,而非“UI 业务逻辑”

缓存其实是后端数据的前端镜像,它天生属于数据层(Service/Api)。

TQ 的设计哲学是让上层(业务组件)通过 queryKey 来感知并操作缓存。这在小项目里很灵活,但在中大型团队里却是一种严重的抽象泄露(Leaky Abstraction)

  • QueryKey 维护灾难: 开发者需要在组件里自由定义 Key。不同页面拼出的 Key 稍有差异,就会导致缓存无法共享或 invalidate 失败。
  • 工程不可控: 你无法限制业务开发者如何定义 Key。在大规模使用后,往往只能靠人工 Review 这种低效手段来兜底。

TQ 的核心维护者也在博客中提及,在大型项目中建议集中维护 QueryKey:tkdodo.eu/blog/effect…

结论: 业务组件的唯一职责是“消费数据”,它不该关心数据是从内存里拿的还是网络里拉的,更不该亲手去管理那个复杂的 queryKey一旦我们同意缓存不该由 UI 层来调度,那么缓存机制就必须向下沉淀。

如果用集中维护 QueryKey 的方式去做 QT 的缓存,本质和在数据层做缓存没有区别,但还在 UI 层配置,整体结构上也显得奇怪。

三、 基础设施的终极形态:透明化

既然缓存被按回了底层,它应该以什么样的姿态存在?在这一点上,经典的计算机系统设计早就给过我们启发:最完美的缓存,是完全透明的。

就像操作系统的 Page Cache 或 CPU 的 L1/L2 Cache:上层应用程序只管发起单纯的 read()write() 指令,内核在底层默默地把缓存调度干好。你永远不需要在业务代码里手动声明:“请把这段逻辑存进二级缓存,Key 叫做 my-data-key”。

中后台的 Api 层缓存也理应如此。它应该像水和空气一样存在于底层的请求管线中,而不是作为一个“高调的工具库”跳到业务组件面前指手画脚。

四、 设想的方案:在 Service 层做声明式配置

对于后端的一个数据是否过期,常见的其实就两种场景:

  • 明确知晓某个操作导致了这个数据的过期(本质是前端触发了某个写操作)
  • 不知道什么操作导致过期,定义个自身可接受的缓存时间
// 接口定义层(Service 层)
const userApi = {
  getList: {
    url: '/api/users',
    cache: {
      ttl: 30_000, // 30秒透明缓存
      // 关键:声明哪些接口操作会触发该接口失效
      invalidatedBy: ['/api/user/save', '/api/user/delete'] 
    }
  },
  save: {
    url: '/api/user/save',
  }
}

框架内部实现极简:

  • 请求时: 自动检查该接口是否有未过期的缓存。
  • 写操作成功时: 根据 invalidatedBy 自动批量失效相关缓存。

对业务层而言:

// 业务组件层
// 开发者完全感知不到缓存的存在,不需要管 Key,不需要手动刷新
const { userList, queryUserListloading } = useAsyncData('userList' userApi.getList);
  • 通过 vue-asyncxuseAsyncData 单独使用 loading、聚焦自动刷新、防抖节流等能力

  • 因为所有失效场景(时间过期 + 相关写操作触发)都已经在 Service/Api 层自动处理好了,业务层几乎不再需要 refresh 函数,也不需要手动 invalidateQueries

    • 在大型项目(数百个页面),长期维护(5年甚至更长时间),多人协作(一份代码历经多人接收)的限制下,业务侧的代码应该尽可能简单,业务侧需要理解的概念应该尽可能的少。

    • 在业务侧代码可能由 AI 接手的背景下,拿走一种变化的维度,长期看系统应该是更优的。

五、 什么时候才可能需要 TQ?

只有在以下极少数例外场景,TQ 的收益才会超过它的架构副作用:

  • 极致的实时交互: 如协同编辑器、大型画布、需要频繁乐观更新(Optimistic Updates)的场景。

总结

TanStack Query 是一个伟大的产品,但它解决的是“状态同步”的难题。而中后台列表/表单业务的核心痛点是**“工程的可预测性”“开发效率”**。

把复杂性留在框架底层,保持业务层的纯净,将缓存彻底做成透明的实现细节——这才是对大型团队长期维护更负责的设计。

你可能真的不需要 TanStack Query。 至少,在大多数后台管理系统中,它的缓存功能不该出现在业务代码的第一线。