查询函数与请求策略
本教程共 38 篇 · 第 7 篇 · 更新于 2026-07-27 · 约 9 分钟阅读
7. 查询函数与请求策略
本节目标:搞懂 queryFn 的返回值和错误处理规则、重试怎么配、查询怎么取消,学完你能掌控请求层各种细节,让请求更健壮。
7.1 queryFn 的返回值
queryFn 是真正发请求的函数,必须返回一个 Promise。这个 Promise 有两条路:
- resolve 数据 — 查询成功
- reject 或 throw — 查询失败
有个容易忽略的规则:resolve 的值不能是 undefined。如果你 resolve 了 undefined,Query 会把它当成失败处理。
为什么这么设计?因为 undefined 在缓存里没法和「没数据」区分。要表示「确实没东西」,用 null:
// 错误:resolve undefined 会被当失败
queryFn: async () => {
const data = await fetchMaybeEmpty()
if (!data) return undefined // 这会让查询失败
return data
}
// 正确:用 null 表示空
queryFn: async () => {
const data = await fetchMaybeEmpty()
if (!data) return null
return data
}
Warning这个坑我踩过。有个接口在某些情况下返回空响应,我图省事直接
return undefined,结果查询一直显示 loading,查了半天才发现是 undefined 的锅。
7.2 错误处理:必须 throw
Query 怎么知道查询失败了?靠 queryFn 抛错或返回 rejected Promise。不抛错的失败,Query 察觉不到。
const { error } = useQuery({
queryKey: ['todos', todoId],
queryFn: async () => {
if (somethingGoesWrong) {
throw new Error('出错了!')
}
if (somethingElseGoesWrong) {
return Promise.reject(new Error('也出错了!'))
}
return data
},
})
throw 和 reject 效果一样,都会被 Query 捕获,存到 error 状态里。
fetch 不会自动抛错
这是最坑新手的点。原生 fetch 在 HTTP 状态码 4xx、5xx 时不会 throw,只有网络错误才 throw。你得手动判断 response.ok:
useQuery({
queryKey: ['todos', todoId],
queryFn: async () => {
const response = await fetch('/todos/' + todoId)
if (!response.ok) {
throw new Error('网络响应异常:' + response.status)
}
return response.json()
},
})
不判断的话,404 返回的 HTML 错误页会被当成数据存进缓存,页面就渲染一堆乱码。
Tipaxios 会自动抛错,省心。用 fetch 的话,建议封装个统一函数处理
response.ok判断,别每个 queryFn 都写一遍。
7.3 QueryFunctionContext
queryFn 接收一个上下文对象,叫 QueryFunctionContext,包含这些字段:
queryKey— 查询键,能从这里取参数client— QueryClient 实例signal— AbortSignal 实例,用于取消请求meta— 可选的元信息
无限查询(Infinite Query)还会多两个:
pageParam— 当前页的参数direction— 方向(已废弃,建议把方向信息放进 pageParam)
最常用的是 queryKey 和 signal。前面讲过从 queryKey 取参数,这里看 signal 的用法。
7.4 重试策略:retry
请求失败时,Query 默认会自动重试 3 次。这个行为可以配置:
// 全局配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3, // 默认就是 3
},
},
})
// 单个查询覆盖
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: 10, // 这个查询重试 10 次
})
retry 的几种写法:
retry: false— 不重试retry: 3— 重试 3 次retry: true— 无限重试(慎用)retry: (failureCount, error) => ...— 自定义逻辑
自定义逻辑很有用,比如根据错误类型决定要不要重试:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: (failureCount, error) => {
// 404 不重试,没意义
if (error.status === 404) return false
// 其他错误最多重试 3 次
return failureCount < 3
},
})
Note
failureCount从 0 开始,第一次重试时是 0。重试过程中的错误会存在failureReason字段里,只有最后一次重试还失败,错误才会进error字段。
服务端默认不重试
v5 有个变更:在服务端(SSR)默认 retry: 0。因为服务端渲染要快,重试会拖慢响应。如果你在 SSR 场景发现查询不重试,这是正常的。
7.5 重试延迟:retryDelay
重试不是立即重试,默认有指数退避:第一次等 1 秒,第二次 2 秒,第三次 4 秒,最多不超过 30 秒。
默认逻辑等价于:
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
可以改:
// 固定延迟
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retryDelay: 1000, // 每次都等 1 秒
})
// 自定义函数
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
})
指数退避是好东西,别随便改成固定延迟。服务短暂抖动时,退避能让服务有时间恢复,避免重试风暴压垮服务器。
7.6 查询取消:AbortSignal
Query 给每个 queryFn 传一个 AbortSignal。当查询过期或失活时,这个 signal 会被 abort,你可以借它取消正在进行的请求。
好处是:用正常的 async/await 写法,就能享受自动取消。
配合 fetch
fetch 原生支持 signal,直接传进去:
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/todos', { signal })
if (!response.ok) throw new Error('请求失败')
return response.json()
},
})
组件卸载或查询失效时,signal 被 abort,fetch 自动取消,不会浪费网络请求。
配合 axios
axios 0.22.0+ 也支持 signal:
useQuery({
queryKey: ['todos'],
queryFn: ({ signal }) =>
axios.get('/todos', { signal }),
})
多个请求共用一个 signal
一个 queryFn 里发多个请求时,把同一个 signal 传给所有请求,一起取消:
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const todosResponse = await fetch('/todos', { signal })
const todos = await todosResponse.json()
// 用同一个 signal 请求每个 todo 的详情
const details = todos.map(({ detailsUrl }) =>
fetch(detailsUrl, { signal }).then((r) => r.json())
)
return Promise.all(details)
},
})
7.7 手动取消查询
除了自动取消,也能手动取消。比如请求很慢,用户想点「取消」按钮:
const query = useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const resp = await fetch('/todos', { signal })
return resp.json()
},
})
const queryClient = useQueryClient()
return (
<button
onClick={() => {
queryClient.cancelQueries({ queryKey: ['todos'] })
}}
>
取消请求
</button>
)
cancelQueries 会 abort 对应的 signal,并让查询状态回退到之前的状态。
cancelQueries 的选项
// 静默取消,不触发错误回调
await queryClient.cancelQueries(
{ queryKey: ['posts'] },
{ silent: true }
)
两个选项:
silent: true— 不传播CancelledError给观察者,默认 falserevert: true— 回退到请求前的状态,默认 true
Warning取消功能在 Suspense 相关的 hook(
useSuspenseQuery等)上不工作。如果你的查询用了 Suspense,别指望取消能生效。
7.8 默认取消行为
补充一个细节:默认情况下,组件卸载时已经发出的请求不会被取消。请求完成后数据会进缓存,下次挂载还能用。
只有你消费了 signal(比如传给 fetch),请求才会被真正取消。这个设计是权衡过的:不消费 signal 时保留数据方便下次用,消费了 signal 就认为你接受取消语义。
7.9 小结
请求层控制就这几样:
- 返回值:resolve 数据别用 undefined,用 null
- 错误:必须 throw 或 reject,fetch 要手动判断
response.ok - 重试:默认 3 次带指数退避,可按错误类型自定义
- 取消:用
signal配合 fetch/axios,或手动cancelQueries
下一篇讲缓存机制,staleTime 和 gcTime 这两个最容易混的概念。