news 2026/9/9 21:11:39

TanStack Query 并行查询(Parallel Queries)完全指南:从手动并行到 useQueries 动态调度

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TanStack Query 并行查询(Parallel Queries)完全指南:从手动并行到 useQueries 动态调度

TanStack Query 并行查询(Parallel Queries)完全指南:从手动并行到 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

TanStack Query(即 React Query)中的「并行查询」指多条查询在同一时刻开始执行,从而最大化数据拉取的并发度。本文围绕 React 框架下的并行查询玩法展开,覆盖手动并行的基本姿势、Suspense 模式下的特殊约束、用于「动态数量」查询的useQueriesuseSuspenseQueries,并结合本仓库的 React Query 源码与测试,讲清并行调度在底层是如何实现的。读完你即可判断场景应该并排写useQuery还是改用useQueries/useSuspenseQueries,并规避类型推断与渲染上的典型坑。

什么是并行查询

「并行查询」是指那些在同一时刻执行(或以最大并发度执行)的查询。在 TanStack Query 的世界里,只要多个查询互不依赖、彼此之间没有「先拿到 A 的结果才去请求 B」的先后关系,它们就可以并行发起,而不是一个接一个地串行等待。

与依赖型查询(参见 依赖查询指南)或串行瀑布(参见 请求瀑布流指南)相对,并行查询的目标是把网络往返「摊平」:多个请求并发地在途,而不是让后续请求排队等待前一个完成。做到这一点的前提是每条查询的queryKey彼此不同;如果两条查询使用完全相同的 key,则共享同一份缓存,只会触发一次网络请求。

手动并行查询:数量固定时的零成本方案

当需要并行的查询数量固定不变时,使用并行查询不需要任何额外工作——只要把任意数量的useQuery(以及useInfiniteQuery)Hook并排放在同一个组件里即可:

function App () { // 下面的三个查询会并行执行 const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) ... }

每条查询都是独立注册的 Query Observer,组件挂载后各自在自身生命周期内发起 fetch,天然互不阻塞。同理,固定数量的无限滚动查询也可以并排使用useInfiniteQuery

function Feeds() { // 两个无限查询并行执行,各自维护独立的 pageParam 与分页状态 const latestPosts = useInfiniteQuery({ queryKey: ['posts', 'latest'], queryFn: ({ pageParam }) => fetchLatestPosts(pageParam), initialPageParam: 0, }) const trendingPosts = useInfiniteQuery({ queryKey: ['posts', 'trending'], queryFn: ({ pageParam }) => fetchTrendingPosts(pageParam), initialPageParam: 0, }) ... }

需要注意的是,这种并行模式仅当查询数量在渲染之间保持稳定时才成立。一旦数量取决于 props、state 或路由参数而动态变化,就受 React「Hooks 必须在每次渲染以相同顺序、相同数量被调用」的规则约束,不能通过Array.map去循环调用useQuery——此时需要切换到下面介绍的useQueries

Suspense 模式下的注意点:并排写法会失效

使用 React Query 的 suspense 模式时,上面的「并排 Hook」并行写法并不奏效。因为第一条useSuspenseQuery会先在组件内部抛出 Promise 使组件挂起(suspend),后续查询还没机会执行。绕开的办法是改用useSuspenseQueries(推荐),或者把每个useSuspenseQuery拆到各自独立的子组件中,由你在组件层级上自己编排并行。

也就是说,在useQuery({ suspense: true })或全局suspense: true的配置下,若同一个组件连续写多个useSuspenseQuery,渲染到第一个挂起点就会中断,后面的查询要等组件重新渲染(数据就绪后)才开始,退化成串行。相关行为详解可参考 Suspense 指南。

动态并行查询:useQueries

如果每次渲染需要执行的查询数量是变化的(例如根据一组用户 ID、一组待拉取的文章 id 来生成查询),就不能用手动并行的方式写死 Hook,那样会违反 Rules of Hooks。此时应使用useQueries,它能动态地并行执行任意多条查询

API 形态与基本用法

useQueries接收一个options 对象,内含一个querieskey,其值为查询对象数组;它返回一个查询结果数组

function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), }) }

queries数组里每一项 query 对象与useQuery接受的选项基本一致(queryKey、queryFn、staleTime、gcTime、refetchInterval、select、retry 等均可使用)。返回的结果数组与输入数组保持一一对应的顺序,其中每一项都是一个标准的查询结果对象,携带statusisPendingisErrorerrordataisFetchingrefetch等字段,可以直接参与渲染:

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> ) }

每个 query 对象还可以带独立的queryClient之外的配置;而自定义QueryClient可以放在useQueries的第二个参数上(或依赖默认的 QueryClientProvider 上下文),而不是逐条传queryClient

底层机制:一个 QueriesObserver 协调多条查询

useQueries并不是 N 个独立 Observer 的简单叠加。查看 React Query 的 useQueries 实现,可以看到它在内部维护了一个来自@tanstack/query-coreQueriesObserver(底层类定义在 queriesObserver.ts):

  • 每次渲染,queries.map会先把每个 query 对象交给client.defaultQueryOptions做一次默认值补齐(如默认staleTimeretry等),得到标准化后的选项数组(useQueries.ts);
  • 随后用React.useState惰性创建唯一一个QueriesObserver,并通过useSyncExternalStore订阅它,从而在任意一条查询的状态变化时以单个批次触发一次组件渲染(得益于notifyManager.batchCalls);
  • 选项变化(比如ids变化导致queries数组内容变化)时通过observer.setQueries热更新,而不会销毁重建整个 Observer。

由此可见,并行背后的调度中心是 query-core 层的QueriesObserver,React 层的useQueries只负责把它接到 React 渲染生命周期上。这与useQuery单个 QueryObserver 的模型对应,是理解整套并行架构的关键。

combine:把多条结果合并成一个返回值

useQueries支持一个顶层combine函数,用于把整组查询结果合并成单一值再返回。典型场景是「整组是否仍在加载 / 是否出错 / 是否在后台刷新」这类聚合判断:

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> ) }

实现上,combine的结果会经过replaceEqualDeep结构化共享,尽可能保持引用稳定(见 queriesObserver.ts 中的 combine 处理),从而让下游组件尽量少做无谓重渲染。需要注意:combine只有在自身引用变化或任一查询结果变化时才会重新执行;把combine内联写在 JSX 每次渲染都会生成新引用,导致每次渲染都重算,因此建议用useCallback包裹或提取为无依赖的稳定函数引用(该建议直接来自 useQueries.ts 的实现注释)。

更多顶层选项与行为细节

结合源码注释与测试(useQueries.test.tsx),还有几个值得注意的细节:

  • subscribed(默认true:设为false时该 Observer 不再订阅查询缓存的更新,适用于只需一次性取值不关心后续变化的场景。测试「should not optimistically show fetching when unsubscribed」验证了subscribed: false时不会乐观地进入 fetching 状态,fetchStatus保持idle,且不会在 query cache 上注册 observer(见 useQueries.test.tsx)。
  • 重复的 queryKeyqueries数组中若多次出现相同 key,部分数据会在这些查询之间共享。官方建议先对查询去重,再把结果映射回需要的结构,避免意外共享(见 useQueries.ts 的 JSDoc)。
  • placeholderData:在useQueries中同样受支持,但它的函数版不会拿到之前渲染的查询信息(previousData/previousQuery恒为undefined),因为查询数量在不同渲染间可能不同。
  • 单条错误:某条查询出错时,默认只让对应项进入error状态而不影响整组;仅当该 query 配置了throwOnError: true(或 suspense 语义下)才会抛出错误(相关行为在 useQueries.test.tsx 的「should throw error if in one of queries' queryFn throws」测试中有覆盖)。
  • 类型层递归上限:为了让每个数组元素的queryFn/select都能独立推断,类型QueriesOptions/QueriesResults会以递归元组方式逐项展开,并设置了 20 层的递归上限(MAXIMUM_DEPTH = 20,见 useQueries.ts)。超出上限或传入元素类型未知的普通数组时,会退化为统一的单一类型推断。

关于这些类型的细节,可查阅 QueriesOptions 类型参考 与 QueriesResults 类型参考。

TypeScript 陷阱:内联 select 无法推断 data

使用 TypeScript 时,如果直接写在传给useQueries的 query 对象上的内联select,它无法从同一对象自身的queryFn推断出data参数类型——会退化回unknown。解决方式是显式标注select的参数类型,或借助queryOptions辅助函数先定义好该查询,从而恢复类型推断。这是 TanStack Query 官方文档标注的一个已知 TypeScript 限制。

原因在于:useQueries是对整个queries数组一次性做类型推断的,内联对象的select无法被同一对象的queryFn进行上下文类型化,于是回退到unknown。三种典型解法如下:

方案一:显式标注 select 参数

const [{ data }] = useQueries({ queries: [{ queryKey: ['post', id], queryFn: fetchPost, select: (data: Post) => data.title, // 显式标注 }], })

方案二:用queryOptions工厂函数预先定型

queryOptions会在对象到达useQueries之前、于单个对象内完成类型解析:

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: title }] = useQueries({ queries: [postOptions(id)], }) return <h1>{title}</h1> }

注意一个进阶细节:展开queryOptions的结果再内联覆盖select依然会退化回unknown——必须把覆盖后的对象再次包进queryOptions,让select在进入useQueries之前被解析(该示例与注释完整记录在 useQueries.ts 的实现 JSDoc 中):

// ❌ data 会是 unknown:展开后又内联 select const [{ data: broken }] = useQueries({ queries: [{ ...postOptions(id), select: (data) => data.title }], }) // ✅ data 推断为 Post 类型 const [{ data: fixed }] = useQueries({ queries: [ queryOptions({ ...postOptions(id), select: (data) => data.title, }), ], })

queryOptions本身是一层极薄的运行时包装——其实现就是直接原样返回传入的 options(见 queryOptions.ts),它的价值完全在于编译期类型标注与跨 Hook/命令式 API 复用。其完整 API 可参考 queryOptions 参考文档。

Suspense 下的动态并行:useSuspenseQueries

对于使用了 Suspense 的动态并行查询,官方推荐直接使用useSuspenseQueries,而不是手动排布多个useSuspenseQuery

从实现上看,useSuspenseQueries.ts 实际上是对useQueries的一层薄封装:它把每个 query 强制加上suspense: truethrowOnError: defaultThrowOnErrorenabled: true,并清空placeholderData,然后原样转交useQueries(见 useSuspenseQueries.ts 的实现)。因此它天然继承了useQueries的动态并行能力,同时让每条结果满足 Suspense 语义:

  • 每个结果对象的data保证已定义(不再需要逐条判断isPending),不存在isPlaceholderData
  • status只会是successerror
  • 多个查询并行发起,全部就绪后才让组件完成挂起,而非逐个挂起(这是它相对多个useSuspenseQuery的核心优势);
  • 同样支持combine,把整组结果合并为单一值(如results.some((r) => r.isFetching)用于渲染「刷新中」指示)。

一个基于用户 ID 列表的动态并行示例:

import { Suspense } from 'react' import { ErrorBoundary } from 'react-error-boundary' import { QueryErrorResetBoundary, useSuspenseQueries, } from '@tanstack/react-query' function Posts({ ids }: { ids: Array<number> }) { // 每个结果都保证 data 已定义,无需逐条 isPending 判断 const postQueries = useSuspenseQueries({ queries: ids.map((id) => ({ queryKey: ['post', id], queryFn: () => fetchPost(id), })), }) return ( <ul> {postQueries.map((query) => ( <li key={query.data.id}>{query.data.title}</li> ))} </ul> ) } function App() { return ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <button onClick={() => resetErrorBoundary()}>Try again</button> </div> )} > <Suspense fallback={<h1>Loading posts...</h1>}> <Posts ids={[1, 2, 3]} /> </Suspense> </ErrorBoundary> )} </QueryErrorResetBoundary> ) }

需要提醒的三条使用约束(来自 useSuspenseQueries.ts 的 JSDoc 与实现):

  1. 错误处理依赖 Error Boundary:首次 fetch 失败且无缓存数据时,查询错误会被抛出,因此<Suspense>外围必须有错误边界;配合QueryErrorResetBoundary才能让用户点击「重试」后恢复。后台 refetch 失败则继续渲染已有缓存数据。
  2. 重新挂载与 staleTime:组件会在所有查询完成后重新挂载(re-mount)。若在等待期间某条查询已经过期(stale),重挂载时它会再次被 fetch。若不想重复请求,应设置足够高的staleTime
  3. 不支持取消(cancellation):suspense 模式下的取消语义不适用于useSuspenseQueries
  4. skipToken被禁止:开发环境下向useSuspenseQueries传入queryFn: skipToken会直接打印错误日志(见 useSuspenseQueries.ts)。

这些行为均有测试佐证,可参考 useSuspenseQueries.test.tsx 中的用例。

并行执行的证据:来自测试与 Observer 层

并行不是「看起来并行」而是真实的并发调度,仓库中的证据链如下:

  • useQueries.test.tsx 的用例「should return the correct states」构造了两条延迟分别为 10ms 与 200ms 的查询,最终断言data1: 1, data2: 2同时就绪;渲染历史results依次为[undefined, undefined] → [1, undefined] → [1, 2],证明二者自渲染起即并发在途、互不等待。
  • query-core 层的 queriesObserver.ts 负责统一管理这组查询,而 useQueries.ts 中每次渲染都会先对全部查询做defaultQueryOptions默认化并调用observer.setQueries完成热同步,从机制上保证了「新增/移除某条查询」不会中断其他查询的状态。

何时选哪种方式

场景推荐方案理由
查询数量固定并排写useQuery/useInfiniteQuery零额外成本,每条查询独立可读
固定数量 + SuspenseuseSuspenseQueries或拆分为独立子组件避免首条查询挂起导致后续查询串行
查询数量动态变化useQueries遵守 Rules of Hooks,按数组驱动
动态数量 + SuspenseuseSuspenseQueries全部并行发起、一次性完成挂起
需要聚合整组状态useQueries/useSuspenseQueriescombine结构化共享、减少不必要重渲染

延伸阅读

  • useQueries Hook 参考 与 useSuspenseQueries Hook 参考
  • 查询(Queries)基础指南、查询键(Query Keys)指南
  • Suspense 指南、请求瀑布流指南(理解并行 vs 串行)、依赖查询指南(理解并行 vs 依赖)
  • queryOptions 参考、QueriesOptions 类型、QueriesResults 类型

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 21:10:16

C8051F340开发板资料实战:原理图、例程与C2调试全解析

简介&#xff1a;C8051F340开发板全套资料以zip包形式提供&#xff0c;内含原理图与源程序&#xff0c;面向嵌入式开发者、电子竞赛备赛者及混合信号微控制器初学者&#xff0c;可帮助解决外设连接、驱动编写和硬件调试难上手的问题。压缩包共590个文件&#xff0c;大小26.34MB…

作者头像 李华
网站建设 2026/9/9 21:10:10

PLC仿真与组态:玻璃冲洗与交通灯联合控制模型

搞自动化项目的人都有个共同痛点&#xff1a;程序写完了&#xff0c;不敢直接往设备上跑。玻璃冲洗线这种带水泵、风机、传送带的产线&#xff0c;逻辑一旦写错&#xff0c;轻则工件卡滞&#xff0c;重则烧接触器。我自己的习惯是&#xff0c;任何新写的PLC程序&#xff0c;先过…

作者头像 李华
网站建设 2026/9/9 21:03:02

STM32+MPU6050四元数姿态解算实战:从原始数据到欧拉角

简介&#xff1a;基于STM32F103与MPU6050六轴惯性测量单元的四元数姿态解算工程&#xff0c;面向嵌入式开发、无人机、平衡车及运动控制等需要实时姿态信息的开发者。程序完整覆盖MPU6050三轴加速度计与三轴陀螺仪数据读取、IO模拟I2C时序、四元数积分与归一化、重力向量比对与…

作者头像 李华
网站建设 2026/9/9 21:02:53

C-MAPSS与LSTM:工业设备剩余寿命预测实战解析

简介&#xff1a;利用长短期记忆网络&#xff08;LSTM&#xff09;在C-MAPSS数据集上实现涡扇发动机剩余寿命预测的Pytorch完整代码包&#xff0c;面向故障预测与健康管理&#xff08;PHM&#xff09;领域的研究者、工业数据分析人员以及深度学习时序建模爱好者。整个资源包共包…

作者头像 李华