TanStack Query 13 ⚛️ Parallel Queries — Executing concurrent requests
A component often needs several independent queries at once: a dashboard that shows user counts, recent orders, and revenue at the same time, or a profile page that fetches the user and the user’s projects in parallel. The previous chapter covered dependent queries, where one query waits for another. This chapter covers the opposite case: queries that have no dependency on each other and should run concurrently. The simplest way to run parallel queries is to call useQuery multiple times in the same component. React Query deduplicates the requests, and each hook has its own state and its own cache entry.
The useQueries hook is the mechanism for running a dynamic number of queries in parallel. When the number of queries is fixed and known at compile time, multiple useQuery calls are the clearest form. When the number of queries depends on a runtime value—the IDs in an array, the servers in a list—useQueries is the correct hook, because the number of useQuery calls cannot change between renders without violating the rules of hooks . The useQueries hook accepts an array of query configurations and returns an array of results in the same order.
Key point: Parallel queries are independent. Each query has its own cache key, its own loading state, and its own error state. The component must handle each state separately, or combine them with a helper that computes the overall loading and error flags. The useQueries hook accepts a combine option that transforms the array of results into a single value, which is useful for computing an aggregate like the total count or the combined error state . The combine function should be stable—defined outside the component or memoized—to avoid recomputing on every render.
Why parallel queries require explicit state handling
The independent-state problem.
A component with three useQuery calls has three isPending flags, three error objects, and three data values. The component must decide what to render while one query is loading and the others are done, and what to render when one query fails and the others succeed. The simplest approach is to render a single loading state when any query is pending, and a single error state when any query has an error, but this hides the progress of the successful queries . The alternative is to render each section with its own loading and error state, which shows the user the parts of the page that are ready.
The dynamic-count problem.
When the number of queries depends on a runtime array, the number of useQuery calls would change between renders, which violates the rules of hooks. The useQueries hook solves this by accepting an array of configurations and returning an array of results. The array can grow or shrink between renders without breaking the hook order, because the hook itself is called once and the array is its argument .
The combine problem.
The combine option is a function that receives the array of results and returns a single value. It is useful for computing an aggregate, such as the sum of the data or a combined error state. The function should be defined outside the component or wrapped in useCallback, because a new function on every render causes the hook to recompute the combined value on every render, which defeats the purpose of memoization .
a. Multiple useQuery calls
The simplest parallel-query pattern is to call useQuery once per independent resource. Each call has its own key, function, and state.
import { useQuery } from '@tanstack/react-query'
function Dashboard() {
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const ordersQuery = useQuery({
queryKey: ['orders'],
queryFn: fetchOrders,
})
const revenueQuery = useQuery({
queryKey: ['revenue'],
queryFn: fetchRevenue,
})
if (usersQuery.isPending) return <p>Loading users...</p>
if (ordersQuery.isPending) return <p>Loading orders...</p>
if (revenueQuery.isPending) return <p>Loading revenue...</p>
return (
<div>
<p>Users: {usersQuery.data.length}</p>
<p>Orders: {ordersQuery.data.length}</p>
<p>Revenue: {revenueQuery.data.total}</p>
</div>
)
}
The three queries run in parallel because they are independent and each one starts when the component mounts. React Query sends the three requests concurrently, and each one resolves independently . The component renders the loading state of the first pending query, which is a simple but sequential-looking approach. A better approach is to render each section with its own loading state, so that the user sees the users count as soon as it is available, even if the orders are still loading.
function Dashboard() {
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const ordersQuery = useQuery({ queryKey: ['orders'], queryFn: fetchOrders })
const revenueQuery = useQuery({ queryKey: ['revenue'], queryFn: fetchRevenue })
return (
<div>
<section>
{usersQuery.isPending ? <Spinner /> : <p>Users: {usersQuery.data.length}</p>}
{usersQuery.isError && <Error message={usersQuery.error.message} />}
</section>
<section>
{ordersQuery.isPending ? <Spinner /> : <p>Orders: {ordersQuery.data.length}</p>}
{ordersQuery.isError && <Error message={ordersQuery.error.message} />}
</section>
<section>
{revenueQuery.isPending ? <Spinner /> : <p>Revenue: {revenueQuery.data.total}</p>}
{revenueQuery.isError && <Error message={revenueQuery.error.message} />}
</section>
</div>
)
}
The per-section approach renders each part of the page as soon as its query resolves, which is a better user experience than a single loading state for the whole page . The queries still run in parallel, and the user sees the available data immediately.
b. The useQueries hook for dynamic counts
The useQueries hook accepts an array of query configurations and returns an array of results. It is the correct hook when the number of queries depends on a runtime value.
import { useQueries } from '@tanstack/react-query'
function UserProfiles({ userIds }: { userIds: number[] }) {
const userQueries = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
})
return (
<div>
{userQueries.map((query, index) => (
<div key={userIds[index]}>
{query.isPending ? (
<Spinner />
) : query.isError ? (
<Error message={query.error.message} />
) : (
<p>{query.data.name}</p>
)}
</div>
))}
</div>
)
}
The userIds.map creates one configuration per ID, and the useQueries hook runs them all in parallel . The userQueries array has the same length as the userIds array, and each result corresponds to the configuration at the same index. The component iterates over the results and renders each one with its own loading and error state.
The combine option transforms the array of results into a single value. It is useful for computing an aggregate.
function UserCount({ userIds }: { userIds: number[] }) {
const combined = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
combine: (results) => ({
total: results.reduce((sum, r) => sum + (r.data?.friends ?? 0), 0),
isLoading: results.some((r) => r.isPending),
isError: results.some((r) => r.isError),
}),
})
if (combined.isLoading) return <Spinner />
if (combined.isError) return <Error />
return <p>Total friends: {combined.total}</p>
}
The combine function receives the array of results and returns an object with the aggregate values. The results.some checks compute the overall loading and error flags, and the results.reduce computes the total . The combine function should be defined outside the component, or wrapped in useCallback, to avoid recomputing the combined value on every render.
c. The combine function and stable references
The combine function is called on every render if it is defined inline. A new function on every render causes the hook to recompute the combined value, which is a performance issue for large result arrays. The function should be defined outside the component or memoized with useCallback.
// Defined outside the component: stable reference
function combineResults(results: UseQueryResult<User>[]) {
return {
total: results.reduce((sum, r) => sum + (r.data?.friends ?? 0), 0),
isLoading: results.some((r) => r.isPending),
isError: results.some((r) => r.isError),
}
}
function UserCount({ userIds }: { userIds: number[] }) {
const combined = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
combine: combineResults,
})
// ...
}
The combineResults function is defined at the module level, so its reference is stable across renders. The hook memoizes the combined value based on the results and the function reference, and it does not recompute unless one of them changes . If the function needs to close over a prop or a state value, it should be wrapped in useCallback with the appropriate dependency array.
The combine function is also useful for returning a stable object that the component can use in a useEffect dependency array. Without combine, the component would have to memoize the aggregate itself, and the array of results is a new reference on every render.
Complete Example Session
// ============================================
// PART 1: THE INDEPENDENT QUERIES
// ============================================
import { useQuery, useQueries } from '@tanstack/react-query'
function Dashboard({ userIds }: { userIds: number[] }) {
// Fixed number of queries: three useQuery calls
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const ordersQuery = useQuery({ queryKey: ['orders'], queryFn: fetchOrders })
// ============================================
// PART 2: THE DYNAMIC QUERIES
// ============================================
// Dynamic number of queries: one per user ID
const profileQueries = useQueries({
queries: userIds.map((id) => ({
queryKey: ['profile', id],
queryFn: () => fetchProfile(id),
})),
combine: (results) => ({
total: results.reduce((sum, r) => sum + (r.data?.followers ?? 0), 0),
isLoading: results.some((r) => r.isPending),
isError: results.some((r) => r.isError),
}),
})
// ============================================
// PART 3: THE PER-SECTION RENDER
// ============================================
return (
<div>
<section>
{usersQuery.isPending ? <Spinner /> : <p>Users: {usersQuery.data.length}</p>}
{usersQuery.isError && <Error message={usersQuery.error.message} />}
</section>
<section>
{ordersQuery.isPending ? <Spinner /> : <p>Orders: {ordersQuery.data.length}</p>}
{ordersQuery.isError && <Error message={ordersQuery.error.message} />}
</section>
// ============================================
// PART 4: THE COMBINED RESULT
// ============================================
<section>
{profileQueries.isLoading ? (
<Spinner />
) : profileQueries.isError ? (
<Error message="Failed to load profiles" />
) : (
<p>Total followers: {profileQueries.total}</p>
)}
</section>
</div>
)
}
The four parts demonstrate the parallel-query patterns: the fixed useQuery calls, the dynamic useQueries call, the per-section render, and the combined result. The critical distinction is between Parts 1 and 2: the fixed queries are separate hooks, and the dynamic queries are a single hook with an array of configurations.
Quick Reference
Fixed vs Dynamic Queries
| Pattern | Hook | Use Case |
|---|---|---|
| Fixed count | Multiple useQuery calls | Known number of resources |
| Dynamic count | useQueries | Number depends on runtime data |
useQueries Options
| Option | Purpose |
|---|---|
queries | Array of query configurations |
combine | Transform the results array |
Result Properties
| Property | Purpose |
|---|---|
data | The query’s data |
isPending | True while loading |
isError | True on error |
error | The error object |
Aggregate Computation
| Function | Purpose |
|---|---|
results.some(r => r.isPending) | Any query loading |
results.some(r => r.isError) | Any query failed |
results.reduce(...) | Sum or other aggregate |
Best Practices
✅ Do This:
// Use multiple useQuery for a fixed number
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) # ✅
// Use useQueries for a dynamic number
const results = useQueries({ queries: ids.map(...) }) # ✅
// Define combine outside the component
function combine(results) { ... } # ✅
// Render per-section states
{query.isPending ? <Spinner /> : <Data />} # ✅
// Use some() for aggregate flags
isLoading: results.some((r) => r.isPending) # ✅
// Use reduce() for aggregates
total: results.reduce((sum, r) => sum + r.data.value, 0) # ✅
❌ Don’t Do This:
// Loop useQuery inside a map
ids.map((id) => useQuery(...)) // Violates rules of hooks # ❌
// Define combine inline
combine: (results) => ({ ... }) // New function every render # ❌
// Single loading state for independent queries
if (anyPending) return <Spinner /> // Hides ready data # ❌
// Ignore per-query errors
return <Data data={query.data} /> // Crashes on error # ❌
// Use useQueries for a fixed number
useQueries({ queries: [q1, q2] }) // useQuery is clearer # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Rules-of-hooks error | useQuery called inside a map | Use useQueries |
| Combine recomputes every render | Function defined inline | Define outside the component |
| Whole page blocked | Single loading state for all queries | Render per-section |
| Missing error state | Only isPending is checked | Add isError checks |
| Result array order mismatch | Results are not reordered | Iterate with the same index |
Real-World Examples
1. Multiple useQuery calls
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const orders = useQuery({ queryKey: ['orders'], queryFn: fetchOrders })
2. useQueries with an array
const results = useQueries({ queries: ids.map((id) => ({ queryKey: ['user', id], queryFn: () => fetchUser(id) })) })
3. Combine total
combine: (results) => results.reduce((sum, r) => sum + (r.data?.count ?? 0), 0)
4. Combine loading
combine: (results) => results.some((r) => r.isPending)
5. Combine error
combine: (results) => results.some((r) => r.isError)
6. Per-section loading
{query.isPending ? <Spinner /> : <Data data={query.data} />}
7. Per-section error
{query.isError && <Error message={query.error.message} />}
8. Stable combine function
function combineResults(results) { ... }
useQueries({ queries, combine: combineResults })
9. Empty array
useQueries({ queries: [] }) // Returns an empty array
10. Conditional queries
queries: ids.map((id) => ({ queryKey: ['user', id], queryFn: () => fetchUser(id), enabled: !!id }))
Visual
Parallel Query Execution
┌──────────────────────────────────────────────────────────────┐
│ INDEPENDENT QUERIES RUN CONCURRENTLY │
│ │
│ Component mounts │
│ │ │
│ ├── usersQuery: GET /api/users ──────────▶ │
│ │ │
│ ├── ordersQuery: GET /api/orders ────────▶ │
│ │ │
│ └── revenueQuery: GET /api/revenue ──────▶ │
│ │
│ Each query resolves independently. │
│ The component renders each section as it becomes ready. │
└──────────────────────────────────────────────────────────────┘
useQueries for Dynamic Counts
┌──────────────────────────────────────────────────────────────┐
│ useQueries ACCEPTS AN ARRAY OF CONFIGURATIONS │
│ │
│ ids = [1, 2, 3] │
│ │ │
│ ▼ │
│ queries = [ │
│ { queryKey: ['user', 1], queryFn: () => fetchUser(1) }, │
│ { queryKey: ['user', 2], queryFn: () => fetchUser(2) }, │
│ { queryKey: ['user', 3], queryFn: () => fetchUser(3) }, │
│ ] │
│ │ │
│ ▼ │
│ results = [result1, result2, result3] │
│ │
│ The results array has the same length and order as the │
│ queries array. │
└──────────────────────────────────────────────────────────────┘
Per-Section vs Single Loading State
┌──────────────────────────────────────────────────────────────┐
│ PER-SECTION RENDERING SHOWS DATA AS IT ARRIVES │
│ │
│ Single loading state: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Users: loading... │ │
│ │ Orders: loading... │ │
│ │ Revenue: loading... │ │
│ │ → user waits for the slowest query │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Per-section state: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Users: 42 ← ready │ │
│ │ Orders: loading... │ │
│ │ Revenue: $1,200 ← ready │ │
│ │ → user sees data as soon as it is available │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Fixed count | Multiple useQuery calls |
| Dynamic count | useQueries |
useQueries input | Array of query configurations |
useQueries output | Array of results |
| Aggregate | combine option |
| Stable combine | Defined outside the component |
| Loading flag | results.some(r => r.isPending) |
| Error flag | results.some(r => r.isError) |
| Per-section rendering | Each query’s state checked separately |
Key takeaways:
- Parallel queries are independent. Each
useQuerycall has its own cache key, its own loading state, and its own error state. The queries run concurrently because none of them depends on the others . The component must handle each state separately, or combine them with an aggregate. useQueriesis for a dynamic number of queries. When the number of queries depends on a runtime array,useQueriesaccepts an array of configurations and returns an array of results. CallinguseQueryinside a map violates the rules of hooks .- The
combineoption transforms the results array into a single value. It is useful for computing aggregates like the total count or the combined loading and error flags. The function should be stable—defined outside the component or wrapped inuseCallback—to avoid recomputing on every render . - Per-section rendering is better than a single loading state. The user sees the data that is ready as soon as it is ready, instead of waiting for the slowest query. The per-section approach renders each query’s loading and error state independently.
- The results array has the same order as the queries array. The component can iterate over the results with the same index as the configurations, which is useful for matching each result to its corresponding item.
- The
combinefunction should not be defined inline. A new function on every render causes the hook to recompute the combined value on every render, which defeats the purpose of the memoization. The function should be defined at the module level or wrapped inuseCallback.
Remember: Parallel queries are the queries that have no dependency on each other. The multiple useQuery calls are the simplest form, and the useQueries hook is the form for a dynamic number of queries. The combine option computes the aggregate, and the per-section rendering shows the data as it arrives. The independent states are the key to the parallel pattern: each query has its own loading, error, and data, and the component decides how to present them. The result is a component that loads its data concurrently and renders it as it becomes available, without blocking the whole page on the slowest request.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!