TL;DR: 乐观更新让 SaaS 应用「感觉瞬间响应」——在服务器响应之前就更新 UI。TanStack Query 的
onMutate回调给了你一个干净的模式:快照当前缓存、应用乐观变更、出错时回滚。本指南覆盖三种模式——单条更新、列表变更和分页列表,附完整 TypeScript 示例。
引言
用户把速度当成产品特性。一次 300 毫秒的服务器调用会显得迟钝。乐观更新消除这种迟钝感:立即显示结果,在后台与服务器对账。
TanStack Query 通过 useMutation hook 的生命周期回调,为这个模式提供了一流支持。
基础乐观更新
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { updateProductFn } from '../server/products'
export function useOptimisticUpdate() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateProductFn,
onMutate: async (newProduct) => {
await queryClient.cancelQueries({ queryKey: ['product', newProduct.id] })
const previous = queryClient.getQueryData(['product', newProduct.id])
queryClient.setQueryData(['product', newProduct.id], newProduct)
return { previous }
},
onError: (err, newProduct, context) => {
queryClient.setQueryData(['product', newProduct.id], context?.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] })
},
})
}
列表变更的乐观更新
对列表查询,你需要操作缓存中的数组:
useMutation({
mutationFn: createProductFn,
onMutate: async (newProduct) => {
await queryClient.cancelQueries({ queryKey: ['products'] })
const previous = queryClient.getQueryData(['products'])
queryClient.setQueryData(['products'], (old: any) => ({
pages: old.pages.map((page: any, i: number) =>
i === 0 ? { ...page, items: [newProduct, ...page.items] } : page
),
pageParams: old.pageParams,
}))
return { previous }
},
onError: (err, _, context) => {
queryClient.setQueryData(['products'], context?.previous)
},
})
回滚决策矩阵
| 失败类型 | UX 响应 | 技术动作 |
|---|---|---|
| 网络错误 | 回滚 + toast | onError 回滚 |
| 校验错误 | 显示字段错误 | 保留乐观状态 + 标记无效 |
| 授权错误 | 回滚 + 重定向 | onError + 路由重定向 |
| 冲突(陈旧数据) | 回滚 + 重新拉取 | onSettled 失效处理 |
结论
乐观更新能大幅改善体感性能。TanStack Query 的 mutation 回调提供了一个干净、可测试的模式,无需手动状态管理的复杂度就能实现。
把乐观更新与正确的缓存失效策略配对,构成完整的数据同步方案。server functions 与 TanStack Query 的集成方式见Server Functions 最佳实践。