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的局部处理不再生效(错误被抛给上层边界了)。