tanstack-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Query v5

TanStack Query v5

Version: @tanstack/react-query@5.90.x Requires: React 18.0+, TypeScript 4.7+
版本:@tanstack/react-query@5.90.x 依赖要求:React 18.0+、TypeScript 4.7+

v5 New Features

v5 新特性

  • useMutationState — cross-component mutation tracking without prop drilling
  • Simplified optimistic updates — via
    variables
    from pending mutations, no cache manipulation needed
  • throwOnError — renamed from
    useErrorBoundary
  • networkMode — offline/PWA support (
    online
    |
    always
    |
    offlineFirst
    )
  • useQueries with combine — merge parallel query results into single object
  • infiniteQueryOptions — type-safe factory for infinite queries (parallel to
    queryOptions
    )
  • maxPages — limit pages in cache for infinite queries (requires bi-directional pagination)
  • Mutation callback signature change (v5.89+)
    onError
    /
    onSuccess
    /
    onSettled
    now receive 4 params (added
    onMutateResult
    )
  • useMutationState — 无需属性透传即可跨组件跟踪变更状态
  • 简化的乐观更新 — 通过待处理变更的
    variables
    实现,无需操作缓存
  • throwOnError — 由
    useErrorBoundary
    重命名而来
  • networkMode — 支持离线/PWA(可选值:
    online
    |
    always
    |
    offlineFirst
  • 带combine的useQueries — 将并行查询结果合并为单个对象
  • infiniteQueryOptions — 用于无限查询的类型安全工厂(与
    queryOptions
    并行)
  • maxPages — 限制无限查询缓存中的页面数量(需要双向分页支持)
  • 变更回调签名变更(v5.89+)
    onError
    /
    onSuccess
    /
    onSettled
    现在接收4个参数(新增
    onMutateResult

Quick Setup

快速设置

bash
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
tsx
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>
  )
}
bash
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
tsx
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分钟
      gcTime: 1000 * 60 * 60,   // 1小时
      refetchOnWindowFocus: false,
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

Unified Devtools (Recommended with Multiple TanStack Libraries)

统一开发者工具(推荐搭配多个TanStack库使用)

If using Query + Router (or other TanStack libraries), use the unified
TanStackDevtools
shell instead of individual devtools components:
bash
npm install -D @tanstack/react-devtools
tsx
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>
  )
}
Use
*Panel
variants (
ReactQueryDevtoolsPanel
,
TanStackRouterDevtoolsPanel
) when embedding inside
TanStackDevtools
.
tsx
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'] })
    },
  })
}
如果同时使用Query和Router(或其他TanStack库),请使用统一的
TanStackDevtools
外壳,而非单独的开发者工具组件:
bash
npm install -D @tanstack/react-devtools
tsx
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 /> },
          // 添加更多插件:Router等
        ]}
      />
    </QueryClientProvider>
  )
}
TanStackDevtools
中嵌入时,请使用
*Panel
变体(如
ReactQueryDevtoolsPanel
TanStackRouterDevtoolsPanel
)。
tsx
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'] })
    },
  })
}

Rule Categories

规则分类

PriorityCategoryRule FileImpact
CRITICALQuery Keys
rules/qk-query-keys.md
Prevents cache bugs and data inconsistencies
CRITICALCaching
rules/cache-configuration.md
Optimizes performance and data freshness
HIGHInvalidation
rules/cache-invalidation.md
Ensures stale data is properly refreshed
HIGHMutations
rules/mut-basics.md
Ensures data integrity after writes
HIGHOptimistic Updates
rules/mut-optimistic-updates.md
Responsive UI during mutations
HIGHError Handling
rules/err-error-handling.md
Prevents poor user experiences
MEDIUMPrefetching
rules/pf-prefetching.md
Improves perceived performance
MEDIUMInfinite Queries
rules/inf-infinite-queries.md
Prevents pagination bugs
MEDIUMSSR/Hydration
rules/ssr-hydration.md
Enables proper server rendering
MEDIUMParallel Queries
rules/parallel-queries.md
Dynamic parallel fetching
LOWPerformance
rules/perf-optimization.md
Reduces unnecessary re-renders
LOWOffline Support
rules/offline-support.md
Enables offline-first patterns
优先级类别规则文件影响
关键查询键
rules/qk-query-keys.md
避免缓存错误和数据不一致
关键缓存
rules/cache-configuration.md
优化性能和数据新鲜度
失效
rules/cache-invalidation.md
确保陈旧数据被正确刷新
变更
rules/mut-basics.md
确保写入后的数据完整性
乐观更新
rules/mut-optimistic-updates.md
变更过程中保持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
实现离线优先模式

Critical Rules

关键规则

Always Do

必须遵循

  • Object syntax for all hooks:
    useQuery({ queryKey, queryFn, ...options })
  • Array query keys:
    ['todos']
    ,
    ['todos', id]
    ,
    ['todos', { filter }]
  • Throw errors in queryFn:
    if (!res.ok) throw new Error('Failed')
  • isPending for initial loading:
    if (isPending) return <Loading />
  • Invalidate after mutations:
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
  • queryOptions factory: reuse across
    useQuery
    ,
    useSuspenseQuery
    ,
    prefetchQuery
  • gcTime (not cacheTime): renamed in v5
  • 所有钩子使用对象语法
    useQuery({ queryKey, queryFn, ...options })
  • 数组形式的查询键
    ['todos']
    ['todos', id]
    ['todos', { filter }]
  • 在queryFn中抛出错误
    if (!res.ok) throw new Error('Failed')
  • 使用isPending判断初始加载状态
    if (isPending) return <Loading />
  • 变更后失效查询
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
  • 使用queryOptions工厂:在
    useQuery
    useSuspenseQuery
    prefetchQuery
    之间复用
  • 使用gcTime(而非cacheTime):v5中已重命名

Never Do

禁止操作

  • v4 array/function syntax:
    useQuery(['todos'], fetchTodos)
    — removed in v5
  • Query callbacks:
    onSuccess
    /
    onError
    /
    onSettled
    removed from queries (still work in mutations) — use
    useEffect
    instead
  • isLoading for "no data yet": meaning changed in v5 — use
    isPending
  • enabled with useSuspenseQuery: not available — use conditional rendering
  • keepPreviousData: removed — use
    placeholderData: keepPreviousData
  • refetch() for changed parameters: include params in queryKey instead, query auto-refetches
  • v4的数组/函数语法
    useQuery(['todos'], fetchTodos)
    — v5中已移除
  • 查询回调:查询中的
    onSuccess
    /
    onError
    /
    onSettled
    已移除(变更中仍可使用)— 改用
    useEffect
  • 使用isLoading判断"无数据"状态:v5中含义已变更 — 改用
    isPending
  • 在useSuspenseQuery中使用enabled:该选项不可用 — 改用条件渲染
  • 使用keepPreviousData:已移除 — 改用
    placeholderData: keepPreviousData
  • 使用refetch()处理参数变更:应将参数包含在查询键中,查询会自动重新获取

v4→v5 Migration Cheatsheet

v4→v5迁移速查表

v4v5Notes
useQuery(['key'], fn, opts)
useQuery({ queryKey, queryFn, ...opts })
Object syntax only
cacheTime
gcTime
Renamed
isLoading
(no data)
isPending
isLoading
=
isPending && isFetching
keepPreviousData: true
placeholderData: keepPreviousData
Import
keepPreviousData
helper
useErrorBoundary
throwOnError
Renamed
onSuccess/onError/onSettled
on queries
RemovedUse
useEffect
for side effects
pageParam = 0
default
initialPageParam: 0
Required for infinite queries
status: 'loading'
status: 'pending'
Renamed
onError(err, vars, ctx)
onError(err, vars, onMutateResult, ctx)
v5.89+ added 4th param
v4v5说明
useQuery(['key'], fn, opts)
useQuery({ queryKey, queryFn, ...opts })
仅支持对象语法
cacheTime
gcTime
已重命名
isLoading
(无数据时)
isPending
isLoading
=
isPending && isFetching
keepPreviousData: true
placeholderData: keepPreviousData
需导入
keepPreviousData
工具函数
useErrorBoundary
throwOnError
已重命名
查询中的
onSuccess/onError/onSettled
已移除使用
useEffect
处理副作用
pageParam = 0
默认值
initialPageParam: 0
无限查询必填
status: 'loading'
status: 'pending'
已重命名
onError(err, vars, ctx)
onError(err, vars, onMutateResult, ctx)
v5.89+新增第4个参数

Known Issues (v5.90.x)

已知问题(v5.90.x)

  • Streaming SSR hydration mismatch
    void prefetchQuery
    +
    useSuspenseQuery
    with conditional
    isFetching
    render causes hydration errors. Workaround:
    await
    prefetch or don't render based on
    fetchStatus
  • useQuery hydration error with prefetching
    useQuery
    + server prefetch can mismatch
    isLoading
    between server/client. Use
    useSuspenseQuery
    instead
  • refetchOnMount ignored for errored queries — errors are always stale. Use
    retryOnMount: false
    in addition to
    refetchOnMount: false
  • useMutationState types
    mutation.state.variables
    typed as
    unknown
    due to fuzzy matching. Cast explicitly in
    select
    callback
  • invalidateQueries only refetches active queries — use
    refetchType: 'all'
    to include inactive queries
  • Readonly query keys break in v5.90.8 — fixed in v5.90.9+
  • 流式SSR水合不匹配
    void prefetchQuery
    +
    useSuspenseQuery
    结合条件
    isFetching
    渲染会导致水合错误。解决方案:
    await
    预获取或不基于
    fetchStatus
    渲染
  • useQuery与预获取的水合错误
    useQuery
    + 服务端预获取可能导致服务端与客户端的
    isLoading
    状态不匹配。改用
    useSuspenseQuery
  • refetchOnMount对错误查询无效 — 错误数据始终视为陈旧。需同时设置
    retryOnMount: false
    refetchOnMount: false
  • useMutationState类型问题
    mutation.state.variables
    被类型化为
    unknown
    ,因模糊匹配导致。需在
    select
    回调中显式转换类型
  • invalidateQueries仅重新获取活跃查询 — 使用
    refetchType: 'all'
    以包含非活跃查询
  • 只读查询键在v5.90.8中失效 — 已在v5.90.9+中修复

Key Patterns

核心模式

tsx
// 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) })
tsx
// 依赖查询(B等待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,
})

// 并行查询
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) }),
})

// 悬停时预获取
const handleHover = () => queryClient.prefetchQuery({ queryKey: ['item', id], queryFn: () => fetchItem(id) })

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

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

// 数据转换
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.filter(t => t.completed) })