tanstack-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Query (React Query) v5

TanStack Query (React Query) v5

Last Updated: 2026-01-20 Versions: @tanstack/react-query@5.90.19, @tanstack/react-query-devtools@5.91.2 Requires: React 18.0+ (useSyncExternalStore), TypeScript 4.7+ (recommended)

最后更新:2026-01-20 版本:@tanstack/react-query@5.90.19, @tanstack/react-query-devtools@5.91.2 依赖要求:React 18.0+(useSyncExternalStore),TypeScript 4.7+(推荐)

v5 New Features

v5 新特性

useMutationState - Cross-Component Mutation Tracking

useMutationState - 跨组件变更追踪

Access mutation state from anywhere without prop drilling:
tsx
import { useMutationState } from '@tanstack/react-query'

function GlobalLoadingIndicator() {
  // Get all pending mutations
  const pendingMutations = useMutationState({
    filters: { status: 'pending' },
    select: (mutation) => mutation.state.variables,
  })

  if (pendingMutations.length === 0) return null
  return <div>Saving {pendingMutations.length} items...</div>
}

// Filter by mutation key
const todoMutations = useMutationState({
  filters: { mutationKey: ['addTodo'] },
})
无需属性透传,即可从任意位置访问变更状态:
tsx
import { useMutationState } from '@tanstack/react-query'

function GlobalLoadingIndicator() {
  // 获取所有待处理的变更
  const pendingMutations = useMutationState({
    filters: { status: 'pending' },
    select: (mutation) => mutation.state.variables,
  })

  if (pendingMutations.length === 0) return null
  return <div>正在保存 {pendingMutations.length} 个条目...</div>
}

// 按变更键筛选
const todoMutations = useMutationState({
  filters: { mutationKey: ['addTodo'] },
})

Simplified Optimistic Updates

简化的乐观更新

New pattern using
variables
- no cache manipulation, no rollback needed:
tsx
function TodoList() {
  const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

  const addTodo = useMutation({
    mutationKey: ['addTodo'],
    mutationFn: (newTodo) => api.addTodo(newTodo),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  // Show optimistic UI using variables from pending mutations
  const pendingTodos = useMutationState({
    filters: { mutationKey: ['addTodo'], status: 'pending' },
    select: (mutation) => mutation.state.variables,
  })

  return (
    <ul>
      {todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
      {/* Show pending items with visual indicator */}
      {pendingTodos.map((todo, i) => (
        <li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li>
      ))}
    </ul>
  )
}
使用
variables
的新模式 - 无需缓存操作,无需回滚:
tsx
function TodoList() {
  const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

  const addTodo = useMutation({
    mutationKey: ['addTodo'],
    mutationFn: (newTodo) => api.addTodo(newTodo),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  // 使用待处理变更中的variables显示乐观UI
  const pendingTodos = useMutationState({
    filters: { mutationKey: ['addTodo'], status: 'pending' },
    select: (mutation) => mutation.state.variables,
  })

  return (
    <ul>
      {todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
      {/* 显示带视觉标识的待处理条目 */}
      {pendingTodos.map((todo, i) => (
        <li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li>
      ))}
    </ul>
  )
}

throwOnError - Error Boundaries

throwOnError - 错误边界

Renamed from
useErrorBoundary
(breaking change):
tsx
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
          <div>
            Error! <button onClick={resetErrorBoundary}>Retry</button>
          </div>
        )}>
          <Todos />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  )
}

function Todos() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: true, // ✅ v5 (was useErrorBoundary in v4)
  })
  return <div>{data.map(...)}</div>
}
useErrorBoundary
重命名而来(破坏性变更):
tsx
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
          <div>
            出错了!<button onClick={resetErrorBoundary}>重试</button>
          </div>
        )}>
          <Todos />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  )
}

function Todos() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: true, // ✅ v5(v4中为useErrorBoundary)
  })
  return <div>{data.map(...)}</div>
}

Network Mode (Offline/PWA Support)

网络模式(离线/PWA支持)

Control behavior when offline:
tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst', // Use cache when offline
    },
  },
})

// Per-query override
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  networkMode: 'always', // Always try, even offline (for local APIs)
})
ModeBehavior
online
(default)
Only fetch when online
always
Always try (useful for local/service worker APIs)
offlineFirst
Use cache first, fetch when online
Detecting paused state:
tsx
const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = offline, waiting for network
离线时控制行为:
tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst', // 离线时使用缓存
    },
  },
})

// 按查询覆盖
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  networkMode: 'always', // 始终尝试,即使离线(适用于本地API)
})
模式行为
online
(默认)
仅在线时获取数据
always
始终尝试(适用于本地/Service Worker API)
offlineFirst
优先使用缓存,在线时获取最新数据
检测暂停状态:
tsx
const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = 离线,等待网络恢复

useQueries with Combine

useQueries 结合结果

Combine results from parallel queries:
tsx
const results = useQueries({
  queries: userIds.map(id => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
  combine: (results) => ({
    data: results.map(r => r.data),
    pending: results.some(r => r.isPending),
    error: results.find(r => r.error)?.error,
  }),
})

// Access combined result
if (results.pending) return <Loading />
console.log(results.data) // [user1, user2, user3]
合并并行查询的结果:
tsx
const results = useQueries({
  queries: userIds.map(id => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
  combine: (results) => ({
    data: results.map(r => r.data),
    pending: results.some(r => r.isPending),
    error: results.find(r => r.error)?.error,
  }),
})

// 访问合并后的结果
if (results.pending) return <Loading />
console.log(results.data) // [user1, user2, user3]

infiniteQueryOptions Helper

infiniteQueryOptions 辅助函数

Type-safe factory for infinite queries (parallel to
queryOptions
):
tsx
import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query'

const todosInfiniteOptions = infiniteQueryOptions({
  queryKey: ['todos', 'infinite'],
  queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// Reuse across hooks
useInfiniteQuery(todosInfiniteOptions)
useSuspenseInfiniteQuery(todosInfiniteOptions)
prefetchInfiniteQuery(queryClient, todosInfiniteOptions)
用于无限查询的类型安全工厂(与
queryOptions
并行):
tsx
import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query'

const todosInfiniteOptions = infiniteQueryOptions({
  queryKey: ['todos', 'infinite'],
  queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// 在钩子中复用
useInfiniteQuery(todosInfiniteOptions)
useSuspenseInfiniteQuery(todosInfiniteOptions)
prefetchInfiniteQuery(queryClient, todosInfiniteOptions)

maxPages - Memory Optimization

maxPages - 内存优化

Limit pages stored in cache for infinite queries:
tsx
useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor, // Required with maxPages
  maxPages: 3, // Only keep 3 pages in memory
})
Note:
maxPages
requires bi-directional pagination (
getNextPageParam
AND
getPreviousPageParam
).

限制无限查询在缓存中存储的页面数量:
tsx
useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor, // 使用maxPages时必填
  maxPages: 3, // 仅在内存中保留3个页面
})
注意:
maxPages
需要双向分页(同时配置
getNextPageParam
getPreviousPageParam
)。

Quick Setup

快速设置

bash
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest
bash
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest

Step 2: Provider + Config

步骤2:Provider + 配置

tsx
// src/main.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 (v5: renamed from cacheTime)
      refetchOnWindowFocus: false,
    },
  },
})

<QueryClientProvider client={queryClient}>
  <App />
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
tsx
// src/main.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小时(v5:从cacheTime重命名)
      refetchOnWindowFocus: false,
    },
  },
})

<QueryClientProvider client={queryClient}>
  <App />
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

Step 3: Query + Mutation Hooks

步骤3:查询与变更钩子

tsx
// src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'

// Query options factory (v5 pattern)
export 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()
  },
})

export function useTodos() {
  return useQuery(todosQueryOptions)
}

export function useAddTodo() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async (newTodo) => {
      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'] })
    },
  })
}

// Usage:
function TodoList() {
  const { data, isPending, isError, error } = useTodos()
  const { mutate } = useAddTodo()

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error: {error.message}</div>
  return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}

tsx
// src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'

// 查询选项工厂(v5模式)
export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: async () => {
    const res = await fetch('/api/todos')
    if (!res.ok) throw new Error('获取失败')
    return res.json()
  },
})

export function useTodos() {
  return useQuery(todosQueryOptions)
}

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

// 使用示例:
function TodoList() {
  const { data, isPending, isError, error } = useTodos()
  const { mutate } = useAddTodo()

  if (isPending) return <div>加载中...</div>
  if (isError) return <div>错误:{error.message}</div>
  return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}

Critical Rules

关键规则

Always Do

必须遵守

Use object syntax for all hooks
tsx
// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })
Use array query keys
tsx
queryKey: ['todos']              // List
queryKey: ['todos', id]          // Detail
queryKey: ['todos', { filter }]  // Filtered
Configure staleTime appropriately
tsx
staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches
Use isPending for initial loading state
tsx
if (isPending) return <Loading />
// isPending = no data yet AND fetching
Throw errors in queryFn
tsx
if (!response.ok) throw new Error('Failed')
Invalidate queries after mutations
tsx
onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}
Use queryOptions factory for reusable patterns
tsx
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)
Use gcTime (not cacheTime)
tsx
gcTime: 1000 * 60 * 60 // 1 hour
所有钩子使用对象语法
tsx
// v5仅支持此方式:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })
使用数组作为查询键
tsx
queryKey: ['todos']              // 列表
queryKey: ['todos', id]          // 详情
queryKey: ['todos', { filter }]  // 筛选后
合理配置staleTime
tsx
staleTime: 1000 * 60 * 5 // 5分钟 - 避免过度重新获取
使用isPending表示初始加载状态
tsx
if (isPending) return <Loading />
// isPending = 尚无数据且正在获取
在queryFn中抛出错误
tsx
if (!response.ok) throw new Error('失败')
变更后使查询失效
tsx
onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}
使用queryOptions工厂实现复用模式
tsx
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)
使用gcTime(而非cacheTime)
tsx
gcTime: 1000 * 60 * 60 // 1小时

Never Do

禁止操作

Never use v4 array/function syntax
tsx
// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌

// v5 (correct):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅
Never use query callbacks (onSuccess, onError, onSettled in queries)
tsx
// v5 removed these from queries:
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {}, // ❌ Removed in v5
})

// Use useEffect instead:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // Do something
  }
}, [data])

// Or use mutation callbacks (still supported):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => {}, // ✅ Still works for mutations
})
Never use deprecated options
tsx
// Deprecated in v5:
cacheTime: 1000 // ❌ Use gcTime instead
isLoading: true // ❌ Meaning changed, use isPending
keepPreviousData: true // ❌ Use placeholderData instead
onSuccess: () => {} // ❌ Removed from queries
useErrorBoundary: true // ❌ Use throwOnError instead
Never assume isLoading means "no data yet"
tsx
// v5 changed this:
isLoading = isPending && isFetching // ❌ Now means "pending AND fetching"
isPending = no data yet // ✅ Use this for initial load
Never forget initialPageParam for infinite queries
tsx
// v5 requires this:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ Required in v5
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
Never use enabled with useSuspenseQuery
tsx
// Not allowed:
useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ Not available with suspense
})

// Use conditional rendering instead:
{id && <TodoComponent id={id} />}
Never rely on refetchOnMount: false for errored queries
tsx
// Doesn't work - errors are always stale
useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,  // ❌ Ignored when query has error
})

// Use retryOnMount instead
useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,
  retryOnMount: false,  // ✅ Prevents refetch for errored queries
  retry: 0,
})

禁止使用v4的数组/函数语法
tsx
// v4(v5中已移除):
useQuery(['todos'], fetchTodos, options) // ❌

// v5(正确方式):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅
禁止使用查询回调(查询中的onSuccess、onError、onSettled)
tsx
// v5已从查询中移除这些:
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {}, // ❌ v5中已移除
})

// 改用useEffect:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // 执行操作
  }
}, [data])

// 或使用变更回调(仍然支持):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => {}, // ✅ 变更中仍可使用
})
禁止使用已废弃的选项
tsx
// v5中已废弃:
cacheTime: 1000 // ❌ 改用gcTime
isLoading: true // ❌ 含义已变更,使用isPending
keepPreviousData: true // ❌ 改用placeholderData
onSuccess: () => {} // ❌ 查询中已移除
useErrorBoundary: true // ❌ 改用throwOnError
不要假设isLoading表示“尚无数据”
tsx
// v5已变更:
isLoading = isPending && isFetching // ❌ 现在表示“待处理且正在获取”
isPending = 尚无数据 // ✅ 用于初始加载
不要忘记无限查询的initialPageParam
tsx
// v5必填:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ v5中必填
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
不要在useSuspenseQuery中使用enabled
tsx
// 不允许:
useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ Suspense模式下不可用
})

// 改用条件渲染:
{id && <TodoComponent id={id} />}
不要依赖refetchOnMount: false处理出错的查询
tsx
// 无效 - 出错的查询始终视为过期
useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,  // ❌ 查询出错时会被忽略
})

// 改用retryOnMount
useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,
  retryOnMount: false,  // ✅ 防止出错的查询在挂载时重新获取
  retry: 0,
})

Known Issues Prevention

已知问题预防

This skill prevents 16 documented issues from v5 migration, SSR/hydration bugs, and common mistakes:
本内容可预防v5迁移、SSR/水合错误以及常见错误中的16个已记录问题

Issue #1: Object Syntax Required

问题1:必须使用对象语法

Error:
useQuery is not a function
or type errors Source: v5 Migration Guide Why It Happens: v5 removed all function overloads, only object syntax works Prevention: Always use
useQuery({ queryKey, queryFn, ...options })
Before (v4):
tsx
useQuery(['todos'], fetchTodos, { staleTime: 5000 })
After (v5):
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000
})
错误
useQuery is not a function
或类型错误 来源v5迁移指南 原因:v5移除了所有函数重载,仅支持对象语法 预防:始终使用`useQuery({ queryKey, queryFn, ...options })
v4写法:
tsx
useQuery(['todos'], fetchTodos, { staleTime: 5000 })
v5写法:
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000
})

Issue #2: Query Callbacks Removed

问题2:查询回调已移除

Error: Callbacks don't run, TypeScript errors Source: v5 Breaking Changes Why It Happens: onSuccess, onError, onSettled removed from queries (still work in mutations) Prevention: Use
useEffect
for side effects, or move logic to mutation callbacks
Before (v4):
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {
    console.log('Todos loaded:', data)
  },
})
After (v5):
tsx
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    console.log('Todos loaded:', data)
  }
}, [data])
错误:回调不执行,TypeScript错误 来源v5破坏性变更 原因:查询中的onSuccess、onError、onSettled已移除(变更中仍可使用) 预防:使用
useEffect
处理副作用,或将逻辑移至变更回调
v4写法:
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {
    console.log('待办事项已加载:', data)
  },
})
v5写法:
tsx
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    console.log('待办事项已加载:', data)
  }
}, [data])

Issue #3: Status Loading → Pending

问题3:状态Loading → Pending

Error: UI shows wrong loading state Source: v5 Migration: isLoading renamed Why It Happens:
status: 'loading'
renamed to
status: 'pending'
,
isLoading
meaning changed Prevention: Use
isPending
for initial load,
isLoading
for "pending AND fetching"
Before (v4):
tsx
const { data, isLoading } = useQuery(...)
if (isLoading) return <div>Loading...</div>
After (v5):
tsx
const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>Loading...</div>
// isLoading = isPending && isFetching (fetching for first time)
错误:UI显示错误的加载状态 来源v5迁移:isLoading重命名 原因
status: 'loading'
重命名为
status: 'pending'
isLoading
含义变更 预防:使用
isPending
表示初始加载,
isLoading
表示“待办且正在获取”
v4写法:
tsx
const { data, isLoading } = useQuery(...)
if (isLoading) return <div>加载中...</div>
v5写法:
tsx
const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>加载中...</div>
// isLoading = isPending && isFetching(首次获取时)

Issue #4: cacheTime → gcTime

问题4:cacheTime → gcTime

Error:
cacheTime is not a valid option
Source: v5 Migration: gcTime Why It Happens: Renamed to better reflect "garbage collection time" Prevention: Use
gcTime
instead of
cacheTime
Before (v4):
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  cacheTime: 1000 * 60 * 60,
})
After (v5):
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  gcTime: 1000 * 60 * 60,
})
错误
cacheTime is not a valid option
来源v5迁移:gcTime 原因:重命名为更能体现“垃圾回收时间”的gcTime 预防:使用
gcTime
替代
cacheTime
v4写法:
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  cacheTime: 1000 * 60 * 60,
})
v5写法:
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  gcTime: 1000 * 60 * 60,
})

Issue #5: useSuspenseQuery + enabled

问题5:useSuspenseQuery + enabled

Error: Type error, enabled option not available Source: GitHub Discussion #6206 Why It Happens: Suspense guarantees data is available, can't conditionally disable Prevention: Use conditional rendering instead of
enabled
option
Before (v4/incorrect):
tsx
useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ Not allowed
})
After (v5/correct):
tsx
// Conditional rendering:
{id ? (
  <TodoComponent id={id} />
) : (
  <div>No ID selected</div>
)}

// Inside TodoComponent:
function TodoComponent({ id }: { id: number }) {
  const { data } = useSuspenseQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
    // No enabled option needed
  })
  return <div>{data.title}</div>
}
错误:类型错误,enabled选项不可用 来源GitHub讨论#6206 原因:Suspense保证数据可用,无法条件禁用 预防:使用条件渲染替代
enabled
选项
v4/错误写法:
tsx
useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ 不允许
})
v5/正确写法:
tsx
// 条件渲染:
{id ? (
  <TodoComponent id={id} />
) : (
  <div>未选择ID</div>
)}

// TodoComponent内部:
function TodoComponent({ id }: { id: number }) {
  const { data } = useSuspenseQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
    // 无需enabled选项
  })
  return <div>{data.title}</div>
}

Issue #6: initialPageParam Required

问题6:initialPageParam必填

Error:
initialPageParam is required
type error Source: v5 Migration: Infinite Queries Why It Happens: v4 passed
undefined
as first pageParam, v5 requires explicit value Prevention: Always specify
initialPageParam
for infinite queries
Before (v4):
tsx
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
After (v5):
tsx
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ Required
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
错误
initialPageParam is required
类型错误 来源v5迁移:无限查询 原因:v4将
undefined
作为首个pageParam,v5要求显式值 预防:始终为无限查询指定
initialPageParam
v4写法:
tsx
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
v5写法:
tsx
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ 必填
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

Issue #7: keepPreviousData Removed

问题7:keepPreviousData已移除

Error:
keepPreviousData is not a valid option
Source: v5 Migration: placeholderData Why It Happens: Replaced with more flexible
placeholderData
function Prevention: Use
placeholderData: keepPreviousData
helper
Before (v4):
tsx
useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  keepPreviousData: true,
})
After (v5):
tsx
import { keepPreviousData } from '@tanstack/react-query'

useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: keepPreviousData,
})
错误
keepPreviousData is not a valid option
来源v5迁移:placeholderData 原因:被更灵活的
placeholderData
函数替代 预防:使用
placeholderData: keepPreviousData
辅助函数
v4写法:
tsx
useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  keepPreviousData: true,
})
v5写法:
tsx
import { keepPreviousData } from '@tanstack/react-query'

useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: keepPreviousData,
})

Issue #8: TypeScript Error Type Default

问题8:TypeScript错误类型默认值

Error: Type errors with error handling Source: v5 Migration: Error Types Why It Happens: v4 used
unknown
, v5 defaults to
Error
type Prevention: If throwing non-Error types, specify error type explicitly
Before (v4 - error was unknown):
tsx
const { error } = useQuery({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw 'custom error string'
    return data
  },
})
// error: unknown
After (v5 - specify custom error type):
tsx
const { error } = useQuery<DataType, string>({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw 'custom error string'
    return data
  },
})
// error: string | null

// Or better: always throw Error objects
const { error } = useQuery({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw new Error('custom error')
    return data
  },
})
// error: Error | null (default)
错误:错误处理中的类型错误 来源v5迁移:错误类型 原因:v4使用
unknown
,v5默认使用
Error
类型 预防:如果抛出非Error类型,显式指定错误类型
v4写法(错误为unknown):
tsx
const { error } = useQuery({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw '自定义错误字符串'
    return data
  },
})
// error: unknown
v5写法(指定自定义错误类型):
tsx
const { error } = useQuery<DataType, string>({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw '自定义错误字符串'
    return data
  },
})
// error: string | null

// 更佳方式:始终抛出Error对象
const { error } = useQuery({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw new Error('自定义错误')
    return data
  },
})
// error: Error | null(默认)

Issue #9: Streaming Server Components Hydration Error

问题9:流式服务端组件水合错误

Error:
Hydration failed because the initial UI does not match what was rendered on the server
Source: GitHub Issue #9642 Affects: v5.82.0+ with streaming SSR (void prefetch pattern) Why It Happens: Race condition where
hydrate()
resolves synchronously but
query.fetch()
creates async retryer, causing isFetching/isStale mismatch between server and client Prevention: Don't conditionally render based on
fetchStatus
with
useSuspenseQuery
and streaming prefetch, OR await prefetch instead of void pattern
Before (causes hydration error):
tsx
// Server: void prefetch
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// Client: conditional render on fetchStatus
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;
After (workaround):
tsx
// Option 1: Await prefetch
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// Option 2: Don't render based on fetchStatus with Suspense
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <div>{data}</div>;  // No conditional on isFetching
Status: Known issue, being investigated by maintainers. Requires implementation of
getServerSnapshot
in useSyncExternalStore.
错误
Hydration failed because the initial UI does not match what was rendered on the server
来源GitHub问题#9642 影响版本:v5.82.0+ 搭配流式SSR(void预取模式) 原因:竞态条件下,
hydrate()
同步解析但
query.fetch()
创建异步重试器,导致服务端与客户端的isFetching/isStale不匹配 预防:使用
useSuspenseQuery
和流式预取时,不要基于
fetchStatus
条件渲染;或使用await预取替代void模式
错误写法(导致水合错误):
tsx
// 服务端:void预取
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// 客户端:基于fetchStatus条件渲染
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;
修复写法:
tsx
// 选项1:Await预取
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// 选项2:使用Suspense时不要基于fetchStatus渲染
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <div>{data}</div>;  // 不要基于isFetching条件渲染
状态:已知问题,维护者正在调查。需要在useSyncExternalStore中实现
getServerSnapshot

Issue #10: useQuery Hydration Error with Prefetching

问题10:预取时useQuery水合错误

Error: Text content mismatch during hydration Source: GitHub Issue #9399 Affects: v5.x with server-side prefetching Why It Happens:
tryResolveSync
detects resolved promises in RSC payload and extracts data synchronously during hydration, bypassing normal pending state Prevention: Use
useSuspenseQuery
instead of
useQuery
for SSR, or avoid conditional rendering based on
isLoading
Before (causes hydration error):
tsx
// Server Component
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// Client Component
function Todos() {
  const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
  if (isLoading) return <div>Loading...</div>;  // Server renders this
  return <div>{data.length} todos</div>;  // Client hydrates with this
}
After (workaround):
tsx
// Use useSuspenseQuery instead
function Todos() {
  const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
  return <div>{data.length} todos</div>;
}
Status: "At the top of my OSS list of things to fix" - maintainer Ephem (Nov 2025). Requires implementing
getServerSnapshot
in useSyncExternalStore.
错误:水合期间文本内容不匹配 来源GitHub问题#9399 影响版本:v5.x 搭配服务端预取 原因
tryResolveSync
在RSC payload中检测到已解析的promise,并在水合期间同步提取数据,绕过了正常的待处理状态 预防:SSR时使用
useSuspenseQuery
替代
useQuery
,或避免基于
isLoading
条件渲染
错误写法(导致水合错误):
tsx
// 服务端组件
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// 客户端组件
function Todos() {
  const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
  if (isLoading) return <div>加载中...</div>;  // 服务端渲染此内容
  return <div>{data.length}个待办事项</div>;  // 客户端水合此内容
}
修复写法:
tsx
// 使用useSuspenseQuery替代
function Todos() {
  const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
  return <div>{data.length}个待办事项</div>;
}
状态:"在我的OSS修复列表顶部" - 维护者Ephem(2025年11月)。需要在useSyncExternalStore中实现
getServerSnapshot

Issue #11: refetchOnMount Not Respected for Errored Queries

问题11:refetchOnMount对出错的查询不生效

Error: Queries refetch on mount despite
refetchOnMount: false
Source: GitHub Issue #10018 Affects: v5.90.16+ Why It Happens: Errored queries with no data are always treated as stale. This is intentional to avoid permanently showing error states Prevention: Use
retryOnMount: false
instead of (or in addition to)
refetchOnMount: false
Before (refetches despite setting):
tsx
const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: () => { throw new Error('Fails') },
  refetchOnMount: false,  // Ignored when query is in error state
  retry: 0,
});
// Query refetches every time component mounts
After (correct):
tsx
const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,
  retryOnMount: false,  // ✅ Prevents refetch on mount for errored queries
  retry: 0,
});
Status: Documented behavior (intentional). The name
retryOnMount
is slightly misleading - it controls whether errored queries trigger a new fetch on mount, not automatic retries.
错误:尽管设置了
refetchOnMount: false
,查询仍在挂载时重新获取 来源GitHub问题#10018 影响版本:v5.90.16+ 原因:无数据的出错查询始终被视为过期。这是为了避免永久显示错误状态的有意设计 预防:使用
retryOnMount: false
替代(或补充)
refetchOnMount: false
错误写法(仍会重新获取):
tsx
const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: () => { throw new Error('失败') },
  refetchOnMount: false,  // 查询出错时会被忽略
  retry: 0,
});
// 每次组件挂载时查询都会重新获取
正确写法:
tsx
const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,
  retryOnMount: false,  // ✅ 防止出错的查询在挂载时重新获取
  retry: 0,
});
状态:已记录的有意行为。
retryOnMount
名称略有误导 - 它控制出错的查询是否在挂载时触发新的获取,而非自动重试。

Issue #12: Mutation Callback Signature Breaking Change (v5.89.0)

问题12:变更回调签名破坏性变更(v5.89.0)

Error: TypeScript errors in mutation callbacks Source: GitHub Issue #9660 Affects: v5.89.0+ Why It Happens:
onMutateResult
parameter added between
variables
and
context
, changing callback signatures from 3 params to 4 Prevention: Update all mutation callbacks to accept 4 parameters instead of 3
Before (v5.88 and earlier):
tsx
useMutation({
  mutationFn: addTodo,
  onError: (error, variables, context) => {
    // context is now onMutateResult, missing final context param
  },
  onSuccess: (data, variables, context) => {
    // Same issue
  }
});
After (v5.89.0+):
tsx
useMutation({
  mutationFn: addTodo,
  onError: (error, variables, onMutateResult, context) => {
    // onMutateResult = return value from onMutate
    // context = mutation function context
  },
  onSuccess: (data, variables, onMutateResult, context) => {
    // Correct signature with 4 parameters
  }
});
Note: If you don't use
onMutate
, the
onMutateResult
parameter will be undefined. This breaking change was introduced in a patch version.
错误:变更回调中的TypeScript错误 来源GitHub问题#9660 影响版本:v5.89.0+ 原因:在
variables
context
之间添加了
onMutateResult
参数,将回调签名从3个参数改为4个 预防:将所有变更回调更新为接受4个参数而非3个
v5.88及更早版本写法:
tsx
useMutation({
  mutationFn: addTodo,
  onError: (error, variables, context) => {
    // context现在是onMutateResult,缺少最终的context参数
  },
  onSuccess: (data, variables, context) => {
    // 同样问题
  }
});
v5.89.0+写法:
tsx
useMutation({
  mutationFn: addTodo,
  onError: (error, variables, onMutateResult, context) => {
    // onMutateResult = onMutate的返回值
    // context = 变更函数上下文
  },
  onSuccess: (data, variables, onMutateResult, context) => {
    // 正确的4参数签名
  }
});
注意:如果不使用
onMutate
onMutateResult
参数将为undefined。此破坏性变更是在补丁版本中引入的。

Issue #13: Readonly Query Keys Break Partial Matching (v5.90.8)

问题13:只读查询键破坏部分匹配(v5.90.8)

Error:
Type 'readonly ["todos", string]' is not assignable to type '["todos", string]'
Source: GitHub Issue #9871 | Fixed in PR #9872 Affects: v5.90.8 only (fixed in v5.90.9) Why It Happens: Partial query matching broke TypeScript types for readonly query keys (using
as const
) Prevention: Upgrade to v5.90.9+ or use type assertions if stuck on v5.90.8
Before (v5.90.8 - TypeScript error):
tsx
export function todoQueryKey(id?: string) {
  return id ? ['todos', id] as const : ['todos'] as const;
}
// Type: readonly ['todos', string] | readonly ['todos']

useMutation({
  mutationFn: addTodo,
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: todoQueryKey('123')
      // Error: readonly ['todos', string] not assignable to ['todos', string]
    });
  }
});
After (v5.90.9+):
tsx
// Works correctly with readonly types
queryClient.invalidateQueries({
  queryKey: todoQueryKey('123')  // ✅ No type error
});
Status: Fixed in v5.90.9. Particularly affected users of code generators like
openapi-react-query
that produce readonly query keys.
错误
Type 'readonly ["todos", string]' is not assignable to type '["todos", string]'
来源GitHub问题#9871 | 已在PR #9872中修复 影响版本:仅v5.90.8(v5.90.9中已修复) 原因:部分查询匹配破坏了只读查询键的TypeScript类型(使用
as const
预防:升级到v5.90.9+,或如果停留在v5.90.8则使用类型断言
v5.90.8写法(TypeScript错误):
tsx
export function todoQueryKey(id?: string) {
  return id ? ['todos', id] as const : ['todos'] as const;
}
// 类型:readonly ['todos', string] | readonly ['todos']

useMutation({
  mutationFn: addTodo,
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: todoQueryKey('123')
      // 错误:readonly ['todos', string] 无法分配给 ['todos', string]
    });
  }
});
v5.90.9+写法:
tsx
// 对只读类型正常工作
queryClient.invalidateQueries({
  queryKey: todoQueryKey('123')  // ✅ 无类型错误
});
状态:已在v5.90.9中修复。尤其影响使用代码生成器(如
openapi-react-query
)生成只读查询键的用户。

Issue #14: useMutationState Type Inference Lost

问题14:useMutationState类型推断丢失

Error:
mutation.state.variables
typed as
unknown
instead of actual type Source: GitHub Issue #9825 Affects: All v5.x versions Why It Happens: Fuzzy mutation key matching prevents guaranteed type inference (same issue as
queryClient.getQueryCache().find()
) Prevention: Explicitly cast types in the
select
callback
Before (type inference doesn't work):
tsx
const addTodo = useMutation({
  mutationKey: ['addTodo'],
  mutationFn: (todo: Todo) => api.addTodo(todo),
});

const pendingTodos = useMutationState({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => {
    return mutation.state.variables;  // Type: unknown
  },
});
After (with explicit cast):
tsx
const pendingTodos = useMutationState({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables as Todo,
});
// Or cast the entire state:
select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>
Status: Known limitation of fuzzy matching. No planned fix.
错误
mutation.state.variables
被推断为
unknown
而非实际类型 来源GitHub问题#9825 影响版本:所有v5.x版本 原因:模糊的查询键匹配导致无法保证类型推断(与
queryClient.getQueryCache().find()
问题相同) 预防:在
select
回调中显式转换类型
错误写法(类型推断不生效):
tsx
const addTodo = useMutation({
  mutationKey: ['addTodo'],
  mutationFn: (todo: Todo) => api.addTodo(todo),
});

const pendingTodos = useMutationState({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => {
    return mutation.state.variables;  // 类型:unknown
  },
});
正确写法(显式转换):
tsx
const pendingTodos = useMutationState({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables as Todo,
});
// 或转换整个状态:
select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>
状态:模糊匹配的已知限制。暂无修复计划。

Issue #15: Query Cancellation in StrictMode with fetchQuery

问题15:StrictMode中fetchQuery的查询取消

Error:
CancelledError
when using
fetchQuery()
with
useQuery
Source: GitHub Issue #9798 Affects: Development only (React StrictMode) Why It Happens: StrictMode causes double mount/unmount. When
useQuery
unmounts and is the last observer, it cancels the query even if
fetchQuery()
is also running Prevention: This is expected development-only behavior. Doesn't affect production
Example:
tsx
async function loadData() {
  try {
    const data = await queryClient.fetchQuery({
      queryKey: ['data'],
      queryFn: fetchData,
    });
    console.log('Loaded:', data);  // Never logs in StrictMode
  } catch (error) {
    console.error('Failed:', error);  // CancelledError
  }
}

function Component() {
  const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
  // In StrictMode, component unmounts/remounts, cancelling fetchQuery
}
Workaround:
tsx
// Keep query observed with staleTime
const { data } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  staleTime: Infinity,  // Keeps query active
});
Status: Expected StrictMode behavior, not a bug. Production builds are unaffected.
错误:使用
fetchQuery()
useQuery
时出现
CancelledError
来源GitHub问题#9798 影响版本:仅开发环境(React StrictMode) 原因:StrictMode导致双重挂载/卸载。当
useQuery
卸载且是最后一个观察者时,即使
fetchQuery()
也在运行,它会取消查询 预防:这是预期的开发环境行为。不影响生产环境
示例:
tsx
async function loadData() {
  try {
    const data = await queryClient.fetchQuery({
      queryKey: ['data'],
      queryFn: fetchData,
    });
    console.log('已加载:', data);  // StrictMode中永远不会打印
  } catch (error) {
    console.error('失败:', error);  // CancelledError
  }
}

function Component() {
  const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
  // StrictMode中,组件卸载/重新挂载,取消fetchQuery
}
解决方法:
tsx
// 使用staleTime保持查询被观察
const { data } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  staleTime: Infinity,  // 保持查询活跃
});
状态:StrictMode的预期行为,不是bug。生产构建不受影响。

Issue #16: invalidateQueries Only Refetches Active Queries

问题16:invalidateQueries仅重新获取活跃查询

Error: Inactive queries not refetching despite
invalidateQueries()
call Source: GitHub Issue #9531 Affects: All v5.x versions Why It Happens: Documentation was misleading -
invalidateQueries()
only refetches "active" queries by default, not "all" queries Prevention: Use
refetchType: 'all'
to force refetch of inactive queries
Default behavior:
tsx
// Only active queries (currently being observed) will refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
To refetch inactive queries:
tsx
queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'all'  // Refetch active AND inactive
});
Status: Documentation fixed to clarify "active" queries. This is the intended behavior.

错误:尽管调用了
invalidateQueries()
,非活跃查询仍未重新获取 来源GitHub问题#9531 影响版本:所有v5.x版本 原因:文档存在误导 - 默认情况下
invalidateQueries()
仅重新获取“活跃”查询,而非“所有”查询 预防:使用
refetchType: 'all'
强制重新获取非活跃查询
默认行为:
tsx
// 仅活跃查询(当前被观察)会重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] });
重新获取非活跃查询:
tsx
queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'all'  // 重新获取活跃和非活跃查询
});
状态:文档已修复以明确“活跃”查询。这是预期行为。

Community Tips

社区技巧

Note: These tips come from community experts and maintainer blogs. Verify against your version.
注意:这些技巧来自社区专家和维护者博客。请根据你的版本验证。

Tip: Query Options with Multiple Listeners

技巧:多监听器的查询选项

Source: TkDodo's Blog - API Design Lessons | Confidence: HIGH Applies to: v5.27.3+
When multiple components use the same query with different options (like
staleTime
), the "last write wins" rule applies for future fetches, but the current in-flight query uses its original options. This can cause unexpected behavior when components mount at different times.
Example of unexpected behavior:
tsx
// Component A mounts first
function ComponentA() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 5000,  // Applied initially
  });
}

// Component B mounts while A's query is in-flight
function ComponentB() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 60000,  // Won't affect current fetch, only future ones
  });
}
Recommended approach:
tsx
// Write options as functions that reference latest values
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: getStaleTime(),  // Evaluated on each render
});
来源TkDodo的博客 - API设计经验 | 可信度:高 适用版本:v5.27.3+
当多个组件使用同一查询但选项不同(如
staleTime
)时,“最后写入获胜”规则适用于未来的获取,但当前进行中的查询使用其原始选项。当组件在不同时间挂载时,这可能导致意外行为。
意外行为示例:
tsx
// 组件A先挂载
function ComponentA() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 5000,  // 初始应用
  });
}

// 组件B在A的查询进行中挂载
function ComponentB() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 60000,  // 不会影响当前获取,仅影响未来获取
  });
}
推荐方式:
tsx
// 将选项编写为引用最新值的函数
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: getStaleTime(),  // 每次渲染时评估
});

Tip: refetch() is NOT for Changed Parameters

技巧:refetch()不应用于参数变更

The
refetch()
function should ONLY be used for refreshing with the same parameters (like a manual "reload" button). For new parameters (filters, page numbers, search terms, etc.), include them in the query key instead.
Anti-pattern:
tsx
// ❌ Wrong - using refetch() for different parameters
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
  queryKey: ['todos'],  // Same key for all pages
  queryFn: () => fetchTodos(page),
});

// This refetches with OLD page value, not new one
<button onClick={() => { setPage(2); refetch(); }}>Next</button>
Correct pattern:
tsx
// ✅ Correct - include parameters in query key
const [page, setPage] = useState(1);
const { data } = useQuery({
  queryKey: ['todos', page],  // Key changes with page
  queryFn: () => fetchTodos(page),
  // Query automatically refetches when page changes
});

<button onClick={() => setPage(2)}>Next</button>  // Just update state
When to use refetch():
tsx
// ✅ Manual refresh of same data (refresh button)
const { data, refetch } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

<button onClick={() => refetch()}>Refresh</button>  // Same parameters

来源避免TanStack Query的常见错误 | 可信度:高
refetch()
函数仅应用于使用相同参数刷新(如手动“重新加载”按钮)。对于新参数(筛选器、页码、搜索词等),应将其包含在查询键中。
反模式:
tsx
// ❌ 错误 - 使用refetch()处理不同参数
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
  queryKey: ['todos'],  // 所有页面使用相同键
  queryFn: () => fetchTodos(page),
});

// 这会使用旧页面值重新获取,而非新值
<button onClick={() => { setPage(2); refetch(); }}>下一页</button>
正确模式:
tsx
// ✅ 正确 - 将参数包含在查询键中
const [page, setPage] = useState(1);
const { data } = useQuery({
  queryKey: ['todos', page],  // 键随页面变更
  queryFn: () => fetchTodos(page),
  // 页面变更时查询自动重新获取
});

<button onClick={() => setPage(2)}>下一页</button>  // 只需更新状态
何时使用refetch():
tsx
// ✅ 手动刷新相同数据(刷新按钮)
const { data, refetch } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

<button onClick={() => refetch()}>刷新</button>  // 相同参数

Key Patterns

关键模式

Dependent Queries (Query B waits for Query A):
tsx
const { data: posts } = useQuery({
  queryKey: ['users', userId, 'posts'],
  queryFn: () => fetchUserPosts(userId),
  enabled: !!user, // Wait for user
})
Parallel Queries (fetch multiple at once):
tsx
const results = useQueries({
  queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})
Prefetching (preload on hover):
tsx
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })
Infinite Scroll (useInfiniteQuery):
tsx
useInfiniteQuery({
  queryKey: ['todos', 'infinite'],
  queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
  initialPageParam: 0, // Required in v5
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
Query Cancellation (auto-cancel on queryKey change):
tsx
queryFn: async ({ signal }) => {
  const res = await fetch(`/api/todos?q=${search}`, { signal })
  return res.json()
}
Data Transformation (select):
tsx
select: (data) => data.filter(todo => todo.completed)
Avoid Request Waterfalls: Fetch in parallel when possible (don't chain queries unless truly dependent)

依赖查询(查询B等待查询A):
tsx
const { data: posts } = useQuery({
  queryKey: ['users', userId, 'posts'],
  queryFn: () => fetchUserPosts(userId),
  enabled: !!user, // 等待用户数据
})
并行查询(同时获取多个):
tsx
const results = useQueries({
  queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})
预取(悬停时预加载):
tsx
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })
无限滚动(useInfiniteQuery):
tsx
useInfiniteQuery({
  queryKey: ['todos', 'infinite'],
  queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
  initialPageParam: 0, // v5中必填
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
查询取消(查询键变更时自动取消):
tsx
queryFn: async ({ signal }) => {
  const res = await fetch(`/api/todos?q=${search}`, { signal })
  return res.json()
}
数据转换(select):
tsx
select: (data) => data.filter(todo => todo.completed)
避免请求瀑布流:尽可能并行获取(除非真正依赖,否则不要链式查询)