跳到主要内容

常见模式

TanStack Query 放进真实项目后,差别往往不在 API 记得多不多,而在几种高频模式是不是已经写顺了。

列表 + 筛选

useQuery({
queryKey: ['users', filters],
queryFn: () => fetchUsers(filters),
})

筛选条件一定要进 queryKey。否则缓存命中会乱。

详情页

useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUserDetail(userId),
enabled: !!userId,
})

列表改完,详情或列表一起刷新

queryClient.invalidateQueries({ queryKey: ['users'] })
queryClient.invalidateQueries({ queryKey: ['user', id] })

分页和无限加载

分页、无限列表是 TanStack Query 很常见的场景,尤其适合交给它管理,因为缓存和翻页状态很容易乱。

预取

进入详情页前,可以先预取下一页或详情数据。

await queryClient.prefetchQuery({
queryKey: ['user', id],
queryFn: () => fetchUserDetail(id),
})

乐观更新

乐观更新最常见的几类场景是:

  • 点赞
  • 勾选完成
  • 切换开关
  • 列表新增
  • 列表删除
  • 拖拽排序

写法 1:只在当前组件里临时顶一个 UI 状态

这种方式最轻,不直接改缓存。

const addTodoMutation = useMutation({
mutationFn: createTodo,
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

const pendingTodo = addTodoMutation.variables

适合:

  • 提交后马上在当前列表里先多渲染一项
  • 不需要马上影响很多页面共享缓存

写法 2:直接改缓存

这是更常见、也更稳的一类。

const mutation = useMutation({
mutationFn: createTodo,
onMutate: async (newTodo, context) => {
await context.client.cancelQueries({ queryKey: ['todos'] })

const previousTodos = context.client.getQueryData(['todos'])

context.client.setQueryData(['todos'], (old = []) => [...old, newTodo])

return { previousTodos }
},
onError: (_error, _variables, onMutateResult, context) => {
context.client.setQueryData(['todos'], onMutateResult.previousTodos)
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
context.client.invalidateQueries({ queryKey: ['todos'] })
},
})

这套写法最核心的 4 步是:

  1. 先取消相关 refetch
  2. 记录旧值
  3. 先改缓存
  4. 失败时回滚,结束后再失效重取

一个更完整的点赞例子

function useToggleLike(postId: string) {
return useMutation({
mutationFn: () => toggleLike(postId),
onMutate: async (_variables, context) => {
await context.client.cancelQueries({ queryKey: ['post', postId] })

const previousPost = context.client.getQueryData(['post', postId]) as
| { id: string; liked: boolean; likeCount: number }
| undefined

context.client.setQueryData(['post', postId], (old: any) => {
if (!old) return old

return {
...old,
liked: !old.liked,
likeCount: old.liked ? old.likeCount - 1 : old.likeCount + 1,
}
})

return { previousPost }
},
onError: (_error, _variables, onMutateResult, context) => {
context.client.setQueryData(['post', postId], onMutateResult.previousPost)
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
context.client.invalidateQueries({ queryKey: ['post', postId] })
context.client.invalidateQueries({ queryKey: ['posts'] })
},
})
}

这个例子比较接近真实业务,因为:

  • 详情缓存要改
  • 列表缓存最终也要同步
  • 失败时要回滚

乐观更新什么时候别急着上

  • 写入失败率高
  • 服务端校验复杂
  • 最终结果不一定和本地猜测一致
  • 同一份数据会被多个复杂列表共享

这类场景里,如果强上乐观更新,后面很容易在缓存一致性上花很多时间。