Skip to content
useQuery 详解

useQuery 详解

useQuery 是 TanStack Query 最核心的 Hook,用于获取、缓存和订阅服务端数据。本章详解其完整 API、所有配置选项和返回值。


完整签名

const result = useQuery({
  queryKey: ['todos'],            // 必填:唯一标识
  queryFn: fetchTodos,            // 必填:返回 Promise 的函数
  // ... 以下均为可选
  staleTime: 5 * 60 * 1000,
  gcTime: 10 * 60 * 1000,
  enabled: true,
  retry: 3,
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
  refetchOnWindowFocus: true,
  refetchOnReconnect: true,
  refetchOnMount: true,
  refetchInterval: false,
  select: (data) => data,
  placeholderData: undefined,
  initialData: undefined,
  throwOnError: false,
  meta: undefined,
  queryClient: undefined,          // v5:可传自定义 QueryClient
})

queryKey — 查询键

查询键是 TanStack Query 缓存的唯一标识。它必须是数组,内容可以是字符串、数字、对象等任何可序列化的值。

// 基本键
useQuery({ queryKey: ['todos'], ... })

// 带参数的键 — 参数变化自动触发新请求
useQuery({ queryKey: ['todo', id], ... })

// 带嵌套对象的键
useQuery({ queryKey: ['todos', { status: 'done', page: 1 }], ... })

💡 最佳实践:查询键从通用到具体排列:['todos', 'list', { status, page }]。键的每一部分都可被 invalidateQueries 模糊匹配。


queryFn — 查询函数

查询函数必须返回一个 Promise(或抛出错误)。它接收一个 QueryFunctionContext 对象:

useQuery({
  queryKey: ['todo', id],
  queryFn: async ({ queryKey, signal, meta }) => {
    // queryKey: ['todo', 1]
    const [, todoId] = queryKey
    const res = await fetch(`/api/todos/${todoId}`, { signal })
    if (!res.ok) throw new Error(`获取失败: ${res.status}`)
    return res.json()
  },
})
属性 类型 说明
queryKey QueryKey 当前查询的完整键数组
signal AbortSignal 用于请求取消,自动传递给 fetch
meta Record<string, unknown> useQuery 中传入的 meta 对象
client QueryClient 当前 QueryClient 实例

🚨 陷阱:查询函数中捕获异常但不重新抛出,TanStack Query 不会认为请求失败。错误必须抛出或返回 rejected Promise

// ❌ 错误被吞掉,TanStack Query 不会感知
queryFn: async () => {
  try { return await fetch('/api/data').then(r => r.json()) }
  catch (e) { console.error(e) }  // 没有 throw!
}

// ✅ 错误正确传播
queryFn: async () => {
  const res = await fetch('/api/data')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
}

所有 Option 参数表

缓存与时效

选项 类型 默认值 说明
staleTime number | Infinity 0 数据被视为"新鲜"的时长(ms)。期间不触发后台重取
gcTime number | Infinity 5 * 60 * 1000 v5 改名自 cacheTime。最后一个 observer 卸载后,缓存存活时长

重取策略

选项 类型 默认值 说明
refetchOnMount boolean | 'always' true 组件挂载时如果数据是 stale,是否重取
refetchOnWindowFocus boolean | 'always' true 窗口聚焦时是否重取 stale 数据
refetchOnReconnect boolean | 'always' true 网络重连时是否重取 stale 数据
refetchInterval number | false | (data, query) => number false 轮询间隔(ms)。如窗口失焦仍产生请求

重试

选项 类型 默认值 说明
retry number | (count, error) => boolean 3 失败重试次数。服务端默认 0
retryDelay number | (attemptIndex, error) => number 指数退避 重试间隔。默认 min(1000 * 2^n, 30000)
retryOnMount boolean true 组件挂载时是否重试失败的查询

行为控制

选项 类型 默认值 说明
enabled boolean true false 时暂停查询(依赖查询、条件查询)
throwOnError boolean | (error, query) => boolean false 错误时是否抛给 ErrorBoundary
networkMode 'online' | 'always' | 'offlineFirst' 'online' 网络模式。offlineFirst 先返回缓存再请求

数据预处理

选项 类型 默认值 说明
select (data: TData) => TSelect undefined 对返回数据做转换,只订阅转换后的结果
initialData TData | () => TData undefined 缓存的初始数据(视为 fresh)。用于已有数据的情况
placeholderData TData | (prev) => TData undefined 查询执行期间的占位数据(不视为 fresh,仍会重取)
structuralSharing boolean | (old, new) => any true 结构共享——保持引用稳定性,减少重渲染

所有返回字段表

const {
  data,              // TData | undefined — 最后一次成功获取的数据
  error,             // TError | null — 请求失败的错误对象
  isPending,         // boolean — 首次加载,尚无数据(v5 改名自 isLoading)
  isFetching,        // boolean — 有请求在进行(含后台更新)
  isLoading,         // boolean — isPending && isFetching(v5 新含义)
  isError,           // boolean — 请求失败
  isSuccess,         // boolean — 请求成功,data 可用
  isStale,           // boolean — 数据已过期
  isFetched,         // boolean — 至少完成过一次 fetch
  isFetchedAfterMount, // boolean — 当前挂载后已完成 fetch
  isRefetching,      // boolean — 正在后台重取(非首次加载)
  isRefetchError,    // boolean — 后台重取失败
  isPaused,          // boolean — 查询因网络离线而暂停
  isPlaceholderData, // boolean — 当前 data 是 placeholderData
  status,            // 'pending' | 'error' | 'success'
  fetchStatus,       // 'fetching' | 'paused' | 'idle'
  dataUpdatedAt,     // number — 数据最后更新的时间戳
  errorUpdatedAt,    // number — 错误最后更新的时间戳
  failureCount,      // number — 当前失败的次数
  failureReason,     // TError | null — 失败原因
  refetch,           // () => Promise<UseQueryResult> — 手动重取
  remove,            // () => void — 从缓存中清除
} = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

status vs fetchStatus

status fetchStatus 场景
pending fetching 首次加载
pending idle enabled: false,尚未 fetch
error idle 请求失败,停止重试
error fetching 请求失败,正在重试
success fetching 后台重取(有旧数据)
success idle 数据 fresh,无请求

select — 数据转换

select 让你只订阅数据的一部分,减少不必要的重渲染:

// ✅ 只订阅 todos 的数量,title 变化不触发重渲染
const { data: todoCount } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (todos) => todos.length,  // 数字是原始值,引用不变
})

// ✅ 筛选已完成项
const { data: doneTodos } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (todos) => todos.filter(t => t.completed),
})

🚨 陷阱select 如果返回一个新引用(如 filter 产生的数组),每次 data 变化都会导致重渲染。对复杂转换,配合 useMemo 或在 queryOptions 工厂中提取稳定的 select 引用。

// ⚠️ 内联匿名函数每次渲染都是新引用 → select 每次都运行
select: (todos) => todos.filter(t => t.completed)

// ✅ 提取为稳定引用
const selectDoneTodos = (todos: Todo[]) => todos.filter(t => t.completed)

function useDoneTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: selectDoneTodos,  // 稳定的函数引用
  })
}

initialData vs placeholderData

维度 initialData placeholderData
被视为什么 来自"合法来源"的缓存数据 仅用于占位的假数据
staleTime 行为 数据被视为 fresh(从该数据创建时开始计时) 数据总是 stale,组件挂载后立即重取
适用场景 从另一个查询获得的数据、SSR 预取数据 加载时的骨架数据、上一页数据
v5 对应 API initialData placeholderData: keepPreviousData
// initialData:从其他查询得到了部分数据
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  initialData: () => {
    // 从用户列表缓存中查找
    const users = queryClient.getQueryData(['users'])
    return users?.find(u => u.id === userId)
  },
})

// placeholderData:查询键变化时保留上一页数据
import { keepPreviousData } from '@tanstack/react-query'

useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: keepPreviousData,  // 翻页时保持上一页数据
})

enabled — 条件查询

enabled: false 暂停查询。常用于依赖查询:

// 等 userId 就绪后才查询
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId,  // userId 为 undefined/null 时不执行
})

throwOnError — 错误边界集成

// 全局:所有 useQuery 出错抛给 ErrorBoundary
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { throwOnError: true },
  },
})

// 局部:只对特定错误抛
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  throwOnError: (error) => error.statusCode >= 500,  // 只有 5xx 抛
})

⚠️ throwOnError: true 后,isError / error 的局部处理不再生效(错误被抛给上层边界了)。