mcpField
T
tanstack-query

tanstack-query

github.com/marcioaltoe开发063 次安装今天更新
代码仓库

关于 tanstack-query

面向使用 React 和 TypeScript 的 TanStack Query v5 用户的参考 Skill,介绍查询与变更模式、缓存、迁移、SSR、离线支持及特定版本问题。

主要功能

  1. 01查询与变更模式
  2. 02缓存配置与失效处理
  3. 03乐观更新与错误处理
  4. 04SSR、分页和离线指导
  5. 05v4 到 v5 迁移说明

适用场景

  1. 01在 React 应用中实现数据获取
  2. 02将项目从 v4 升级到 v5
  3. 03配置查询缓存和刷新
  4. 04排查水合或变更行为问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/marcioaltoe/skills --skill tanstack-query

安装后在 Claude Code 中输入/tanstack-query确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
rules/
cache-configuration.md
cache-invalidation.md
err-error-handling.md
inf-infinite-queries.md
mut-basics.md
mut-optimistic-updates.md
offline-support.md
parallel-queries.md
perf-optimization.md
pf-prefetching.md
qk-query-keys.md
ssr-hydration.md

详细介绍

TanStack Query v5

版本:@tanstack/react-[email protected] 要求:React 18.0+,TypeScript 4.7+

v5 新功能

  • useMutationState — 跨组件跟踪 mutation,无需逐层传递 props
  • 简化乐观更新 — 使用待处理 mutation 的 variables,无需操作缓存
  • throwOnError — 从 useErrorBoundary 重命名而来
  • networkMode — 支持离线/PWA(online | always | offlineFirst)
  • 支持 combine 的 useQueries — 将并行查询结果合并为单个对象
  • infiniteQueryOptions — 用于无限查询的类型安全工厂(与 queryOptions 对应)
  • maxPages — 限制无限查询在缓存中的页面数量(需要双向分页)
  • Mutation 回调签名变更(v5.89+) — onError/onSuccess/onSettled 现在接收 4 个参数(新增 onMutateResult)

快速设置

npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 min
      gcTime: 1000 * 60 * 60,   // 1 hour
      refetchOnWindowFocus: false,
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
统一 Devtools(建议与多个 TanStack 库搭配使用)

如果同时使用 Query + Router(或其他 TanStack 库),请使用统一的 TanStackDevtools 外壳,而不是单独的 devtools 组件:

npm install -D @tanstack/react-devtools
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <TanStackDevtools
        config={{ position: 'bottom-right' }}
        plugins={[
          { name: 'TanStack Query', render: <ReactQueryDevtoolsPanel /> },
          // Add more plugins: Router, etc.
        ]}
      />
    </QueryClientProvider>
  )
}

在 TanStackDevtools 内嵌时,请使用 *Panel 变体(ReactQueryDevtoolsPanel、TanStackRouterDevtoolsPanel)。

import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'

const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: async () => {
    const res = await fetch('/api/todos')
    if (!res.ok) throw new Error('Failed to fetch')
    return res.json()
  },
})

function useTodos() {
  return useQuery(todosQueryOptions)
}

function useAddTodo() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async (newTodo: { title: string }) => {
      const res = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newTodo),
      })
      if (!res.ok) throw new Error('Failed to add')
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

规则类别

优先级类别规则文件影响
严重查询键rules/qk-query-keys.md防止缓存错误和数据不一致
严重缓存rules/cache-configuration.md优化性能和数据新鲜度
高失效rules/cache-invalidation.md确保过期数据得到正确刷新
高Mutationsrules/mut-basics.md确保写入后的数据完整性
高乐观更新rules/mut-optimistic-updates.mdmutation 期间保持 UI 响应灵敏
高错误处理rules/err-error-handling.md防止糟糕的用户体验
中预取rules/pf-prefetching.md提升感知性能
中无限查询rules/inf-infinite-queries.md防止分页错误
中SSR/水合rules/ssr-hydration.md支持正确的服务端渲染
中并行查询rules/parallel-queries.md动态并行获取数据
低性能rules/perf-optimization.md减少不必要的重新渲染
低离线支持rules/offline-support.md支持离线优先模式

关键规则

务必做到
  • 所有 Hook 均使用对象语法:useQuery({ queryKey, queryFn, ...options })
  • 查询键使用数组:['todos']、['todos', id]、['todos', { filter }]
  • 在 queryFn 中抛出错误:if (!res.ok) throw new Error('Failed')
  • 初始加载时使用 isPending:if (isPending) return <Loading />
  • mutation 后使查询失效:onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
  • 使用 queryOptions 工厂:在 useQuery、useSuspenseQuery、prefetchQuery 之间复用
  • 使用 gcTime(而不是 cacheTime):v5 中已重命名
切勿这样做
  • 使用 v4 数组/函数语法:useQuery(['todos'], fetchTodos) — 已在 v5 中移除
  • 使用查询回调:查询中的 onSuccess/onError/onSettled 已移除(mutation 中仍可用)——请改用 useEffect
  • 用 isLoading 表示“尚无数据”:其含义在 v5 中已变更——请使用 isPending
  • 在 useSuspenseQuery 中使用 enabled:不可用——请使用条件渲染
  • 使用 keepPreviousData:已移除——请使用 placeholderData: keepPreviousData
  • 参数变更时调用 refetch():请改为将参数包含在 queryKey 中,查询会自动重新获取

v4→v5 迁移速查表

v4v5备注
useQuery(['key'], fn, opts)useQuery({ queryKey, queryFn, ...opts })仅支持对象语法
cacheTimegcTime已重命名
isLoading(无数据时)isPendingisLoading = isPending && isFetching
keepPreviousData: trueplaceholderData: keepPreviousData导入 keepPreviousData 辅助函数
useErrorBoundarythrowOnError已重命名
查询上的 onSuccess/onError/onSettled已移除使用 useEffect 处理副作用
pageParam = 0 默认值initialPageParam: 0无限查询必需
status: 'loading'status: 'pending'已重命名
onError(err, vars, ctx)onError(err, vars, onMutateResult, ctx)v5.89+ 新增第 4 个参数

已知问题(v5.90.x)

  • 流式 SSR 水合不匹配——void prefetchQuery + useSuspenseQuery 搭配基于 isFetching 条件渲染会导致水合错误。解决方法:对预取使用 await,或不要基于 fetchStatus 进行渲染
  • 预取时 useQuery 水合错误——useQuery + 服务端预取可能导致服务端与客户端的 isLoading 不匹配。改用 useSuspenseQuery
  • refetchOnMount 对出错的查询无效——错误始终处于过期状态。除设置 refetchOnMount: false 外,还要设置 retryOnMount: false
  • useMutationState 类型——由于模糊匹配,mutation.state.variables 的类型为 unknown。在 select 回调中显式进行类型转换
  • invalidateQueries 仅重新获取活跃查询——使用 refetchType: 'all' 以包含非活跃查询
  • 只读查询键在 v5.90.8 中失效——已在 v5.90.9+ 修复

关键模式

// Dependent queries (B waits for A)
const { data: user } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const { data: posts } = useQuery({
  queryKey: ['posts', user?.id],
  queryFn: () => fetchPosts(user!.id),
  enabled: !!user,
})

// Parallel queries
const results = useQueries({
  queries: ids.map(id => ({ queryKey: ['item', id], queryFn: () => fetchItem(id) })),
  combine: (results) => ({ data: results.map(r => r.data), pending: results.some(r => r.isPending) }),
})

// Prefetch on hover
const handleHover = () => queryClient.prefetchQuery({ queryKey: ['item', id], queryFn: () => fetchItem(id) })

// Infinite scroll
useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// Query cancellation
queryFn: async ({ signal }) => {
  const res = await fetch(`/api/search?q=${query}`, { signal })
  return res.json()
}

// Data transformation
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.filter(t => t.completed) })

评论

支持 Markdown