TanStack Query React 的 useQueries:批量并发查询、结果合并与类型推导实战指南
2026/9/10 3:35:36 网站建设 项目流程

TanStack Query React 的 useQueries:批量并发查询、结果合并与类型推导实战指南

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

导读

useQueries是 TanStack Query 中用于一次性获取数量可变的一组查询的核心 Hook,典型的应用场景是"根据一组动态 id 批量拉取详情数据"。本文基于 react-query 源码 与 query-core 的 QueriesObserver 实现,完整讲解它的签名、参数语义、combine结果合并与引用稳定性、subscribed订阅控制、placeholderData行为差异,以及 inlineselect类型推导失效这一知名 TypeScript 限制及其解决方案。读完本文,你将能写出类型安全、性能可控的批量查询代码,并理解其底层运行机制。


1. 函数签名与核心定位

useQueries的定义位于 react-query/src/useQueries.ts:354,签名如下:

function useQueries< T extends Array<any>, TCombinedResult = QueriesResults<T>, >( { queries, combine?, subscribed?, }: { queries: | readonly [...QueriesOptions<T>] | readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> }] combine?: (result: QueriesResults<T>) => TCombinedResult subscribed?: boolean }, queryClient?: QueryClient, ): TCombinedResult

核心定位与useQuery的区别在于:

  • 数量可变queries接收一个"查询选项对象数组",每个对象的形态与useQuery的选项几乎完全一致,唯一例外是每个查询内不接受queryClient选项——因为QueryClient是在顶层作为第二个参数传入的(或从最近的上下文 Provider 中获取)。
  • 批量执行:底层不是简单地多次调用useQuery,而是实例化一个 QueriesObserver,由它统一管理一组QueryObserver
  • 返回结果数组:未传combine时,返回值是查询结果数组,顺序与输入queries数组一一对应;传入combine后,返回combine的返回值。

在 useQueries.test.tsx:25-64 中可以看到最基础的行为验证:两个不同 queryKey 的查询分别以不同延迟返回数据,useQueries在三次渲染中依次产出[{data:undefined},{data:undefined}][{data:1},{data:undefined}][{data:1},{data:2}],证明结果数组始终与输入顺序严格对应,且每个查询独立演进自己的状态机。


2. 基础用法:动态批量查询

文档给出的第一个示例是最典型的场景——根据一组动态id拉取文章列表。源码中queries: ids.map(...)每次渲染都会生成一个新数组,这正好展示了useQueries处理"数量可变"查询的灵活性:

import { useQueries } from '@tanstack/react-query' function Posts({ ids }: { ids: Array<number> }) { const postQueries = useQueries({ queries: ids.map((id) => ({ queryKey: ['post', id], queryFn: () => fetchPost(id), staleTime: Infinity, })), }) return ( <ul> {postQueries.map((query, index) => { if (query.isPending) return <li key={ids[index]}>Loading...</li> if (query.isError) return <li key={ids[index]}>Error: {query.error.message}</li> return <li key={ids[index]}>{query.data.title}</li> })} </ul> ) }

2.1 去重与数据共享的注意事项

文档明确警告:如果queries数组中出现相同的 query key 不止一次,可能会导致数据在查询之间被共享(同一个QueryObserver被复用)。在开发环境下,QueriesObserver.setQueries 会检测到重复 queryHash 并输出控制台警告:

[QueriesObserver]: Duplicate Queries found. This might result in unexpected behavior.

从源码 queriesObserver.ts:261-294 的#findMatchingObservers可以看到:新查询会按queryHash去匹配并复用已有的 observer,相同 hash 的查询共享同一个 observer 状态。因此文档建议:先去重查询,再把结果映射回你期望的结构。例如ids本身有重复值时,应先unique再去map

2.2 内部运行机制(源码视角)

useQueries.ts:390-451 展示了 Hook 的主流程:

  1. defaultedQueries通过client.defaultQueryOptions为每个查询选项补充默认值,并根据isRestoring/subscribed设置_optimisticResults,保证"结果在订阅前就已处于拉取状态";
  2. useState(() => new QueriesObserver(client, defaultedQueries, options))只创建一次 observer,之后通过useEffect中的observer.setQueries(...)同步最新的查询选项;
  3. 借助useSyncExternalStore订阅 observer(订阅逻辑经过notifyManager.batchCalls批量合并通知)。

这也解释了为什么useQueries支持数量动态变化的查询:每次渲染setQueries都会调用#findMatchingObservers做 observer 复用与回收(queriesObserver.ts:100-151),只有长度或索引发生变化(hasStructuralChange)时才销毁/新建 observer。


3. 使用 combine 合并结果为单一值

多个查询的独立结果数组在多数场景下并不好用,combine选项可以把它们合并成一个聚合值,并做结构共享(structural sharing),使其引用尽可能稳定

import { useQueries } from '@tanstack/react-query' function Posts({ ids }: { ids: Array<number> }) { const { data, isPending, isError } = useQueries({ queries: ids.map((id) => ({ queryKey: ['post', id], queryFn: () => fetchPost(id), })), combine: (postQueries) => { return { data: postQueries.map((query) => query.data), isPending: postQueries.some((query) => query.isPending), isError: postQueries.some((query) => query.isError), } }, }) if (isPending) return 'Loading...' if (isError) return 'Error loading posts' return ( <ul> {data.map((post) => ( <li key={post?.id}>{post?.title}</li> ))} </ul> ) }

3.1 combine 的底层实现:何时重新执行

#combineResult(queriesObserver.ts:215-248)是核心逻辑。只有当以下任一条件满足时,combine才会重新执行:

  1. 底层原始结果#result的引用发生了改变(即某个查询的结果真的更新了);
  2. 查询的queryHash列表发生了变化(查询数量或 key 变了);
  3. combine函数本身的引用发生了变化

而计算出的聚合结果会经过replaceEqualDeep(深度相等比较)处理,若新旧聚合值在结构上相等,则复用旧引用——这就是文档所说"结果会被结构共享,尽可能保持引用稳定"的含义。测试 useQueries.test.tsx:817-871 验证了"combine 返回稳定引用时组件不重新渲染、返回不同引用时才重新渲染"。

3.2 性能陷阱:内联 combine 每次渲染都会执行

文档特别强调:内联定义的combine每次渲染都会重新执行,因为每次渲染它的函数引用都不同(满足上述条件 3)。测试 useQueries.test.tsx:674-737 实测了"稳定引用 combine 的优化":用React.useCallback包裹后,无关重渲染不会触发combine重新执行(useQueries.test.tsx:993-1066 有专门用例);而引用变化时确实会重跑(useQueries.test.tsx:753-813)。

因此生产代码应遵循:

const combine = React.useCallback((postQueries: Array<QueryObserverResult>) => { return { data: postQueries.map((query) => query.data), isPending: postQueries.some((query) => query.isPending), isError: postQueries.some((query) => query.isError), } }, [])

combine没有依赖,也可以直接提取为模块级稳定函数。此外还有两个相关的工程细节:

  • 属性跟踪#trackResult(queriesObserver.ts:193-213)会把组件实际访问过的属性在所有 observer 上同步跟踪,保证渲染最小化;测试 useQueries.test.tsx:490 和 useQueries.test.tsx:926 验证了"通过 combine 也能正确进行属性跟踪"。
  • 过期闭包问题:测试 useQueries.test.tsx:630-673 覆盖了 #6648 的 stale closure 回归用例,确保 combine 不会读到旧状态。

4. subscribed:控制对查询缓存的订阅

subscribeduseQueries顶层的布尔选项,默认true

取值行为
true(默认)observer 订阅查询缓存更新,结果会随缓存变化而更新
false该 observer取消订阅查询缓存的更新

在源码 useQueries.ts:388 中,const subscribed = options.subscribed !== false,随后在 useQueries.ts:398-402 影响_optimisticResults的取值,并在 useQueries.ts:433-444 决定是否真正执行observer.subscribe(...)(不订阅时退化为noop)。

注意:subscribed只在useQueries顶层接受,单个查询对象内不接受该选项——这是与useQuery选项集的一个显著差异。测试 useQueries.test.tsx:66-92 验证了subscribed: false时:queryFn不会被调用、isFetchingfalsefetchStatusidle,且查询缓存上不挂载任何 observer。


5. placeholderData:与 useQuery 的行为差异

useQueries同样支持placeholderData,但存在两个与useQuery不同的细节(见 useQueries.ts:42-52 的类型定义):

  1. 签名不同:这里接受的是QueriesPlaceholderDataFunction,其回调接收的previousDatapreviousQuery永远是undefined,而useQuery的 placeholder 函数会收到前一次渲染的数据与查询。
  2. 原因:因为queries数组的长度和内容在不同渲染之间可能完全不同,useQueries无法像useQuery那样为每个查询提供"上一次渲染的同 key 查询信息"。

换句话说,在useQueriesplaceholderData只能提供静态占位值(或基于参数自身计算的函数形式),不能跨渲染复用先前数据。


6. 类型参数与返回值类型推导

6.1 泛型参数

参数约束/默认说明
T必须是数组(Array<any>queries数组的完整类型,驱动后续所有推导
TCombinedResult默认QueriesResults<T>combine时即查询结果数组类型;有combine时为它的返回类型

6.2 QueriesOptions / QueriesResults 的递归推导

从 useQueries.ts:156-223 可以看到两个对外导出的工具类型:

  • QueriesOptions<T>:把每个元素逐个解包(unwrapping),让每个查询对象的queryFn/select/throwOnError各自独立推导,最多递归 20 个元素(MAXIMUM_DEPTH = 20,useQueries.ts:55)。
  • QueriesResults<T>:与前者镜像,把每个元素映射为各自的GetUseQueryResult<...>

推导规则分三部分(useQueries.ts:60-142):

  1. 若传入显式类型参数(对象形态{ queryFnData, error, data }或元组形态[TQueryFnData, TError, TData]),按显式类型映射;
  2. 否则从queryFnselectthrowOnError中推断出TQueryFnDataTDataTError
  3. 兜底为默认类型。

数组元素的特性还体现在:

  • 空数组T extends []→ 返回[]
  • 不定长数组(如unknown[])→ 原样返回;
  • 超过 20 个元素的元组或同构数组→ 回退为单一的同构UseQueryOptionsForUseQueries/UseQueryResult类型。

另外,若查询对象设置了非 undefined 的initialData,对应的结果类型会升级为DefinedUseQueryResult(见GetDefinedOrUndefinedQueryResult,useQueries.ts:97-111),从而保证data非空。

6.3 返回值

  • combine:返回QueriesResults<T>,即与输入顺序一致的结果数组;
  • combine:返回TCombinedResultcombine的返回值)。

7. inline select 的类型推导限制与 queryOptions 解法

这是useQueries最知名的类型陷阱(对应 TanStack Query issue #6556)。文档明确指出:useQuery不同,useQueries无法从内联查询对象自身的queryFn推导出同一对象内联select的参数类型。原因在于useQueries一次性对整个queries数组做整体类型推导,内联对象的select参数无法从同对象queryFn获得上下文类型(contextual typing),于是退化为unknown

同样的限制也适用于 useSuspenseQueries。

两种解决方式:

方式一:显式标注select参数类型(最直接,但类型需手写):

select: (data: Post) => data.title

方式二:用queryOptions预先解析类型(推荐)。把查询选项封装进queryOptions(),其类型会在单个对象内部先完成解析,再整体交给useQueries,从而绕开数组级推导的限制(见 queryOptions 参考文档):

import { queryOptions, useQueries } from '@tanstack/react-query' const postOptions = (id: number) => queryOptions({ queryKey: ['post', id], queryFn: () => fetchPost(id), }) function PostTitle({ id }: { id: number }) { const [{ data: broken }] = useQueries({ queries: [ { ...postOptions(id), // ❌ `data` 在这里是 `unknown`:先展开 queryOptions 再覆盖 select, // 覆盖发生在 useQueries 整体推导之前,select 参数拿不到类型 select: (data) => data.title, }, ], }) const [{ data: fixed }] = useQueries({ queries: [ queryOptions({ ...postOptions(id), // ✅ `data` 在这里是 `Post`:整个对象在 queryOptions 内部完成类型解析 select: (data) => data.title, }), ], }) return <h1>{fixed}</h1> }

注意文档与示例代码共同强调的一个细节:即使先用queryOptions生成选项,再对其展开(spread)并内联覆盖select,类型依然会退化为unknown——因为覆盖行为发生在对象到达useQueries之前、类型解析之外。正确做法是把整个对象(含覆盖后的select)再次包进queryOptions,让最终形态在单对象层面完成类型解析。


8. 常见问题速查

问题结论依据
数组中出现重复 queryKey数据可能被共享,先unique去重再映射结果queriesObserver.ts:89-98 开发环境会告警
内联combine每次渲染都执行是,用useCallback或模块级稳定函数包裹queriesObserver.ts:228-232;测试 useQueries.test.tsx:993-1066
placeholderData能否拿到上次数据不能,previousData/previousQuery恒为undefineduseQueries.ts:42-52
内联select参数类型为unknownqueryOptions包裹或显式标注参数类型useQueries.ts:243-249
超过 20 个查询的元组类型推导回退为同构类型,仍可工作useQueries.ts:160-161
subscribed: false时查询是否发起不发起,queryFn不会被调用测试 useQueries.test.tsx:66-92

9. 延伸阅读

  • Hook 完整实现:packages/react-query/src/useQueries.ts
  • 底层多查询观察者:packages/query-core/src/queriesObserver.ts
  • 行为测试用例:packages/react-query/src/tests/useQueries.test.tsx
  • 类型测试(编译期类型行为验证):packages/react-query/src/tests/useQueries.test-d.tsx
  • 相关 API:queryOptions | useSuspenseQueries
  • 其他框架的同名实现(原理一致,API 略异):preact 版 useQueries、solid 版 useQueries、vue 版 useQueries

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询