useQueries in React: Fetch Multiple Resources at Once
The useQueries hook fetches multiple independent resources in parallel and returns an array of query results. Instead of nesting multiple useQuery calls (which works but is verbose), useQueries accepts an array of query configurations and returns an array of results, enabling elegant code for multi-resource pages. This article teaches when to use useQueries, how to combine results across queries, and patterns for handling partial failures when some queries succeed and others fail.
I used to call useQuery three times for a user profile page: one for the user, one for their posts, and one for their followers. Three separate hooks, three separate variables to track. Switching to useQueries halved the boilerplate while making it obvious that these fetches happen in parallel. It became the default pattern on my team after that.
What Is useQueries and When to Use It
useQueries accepts an array of query configurations and returns an array of query results. Each result has the same structure as a useQuery return value ({ data, isLoading, error, etc. }):
import { useQueries } from '@tanstack/react-query';
function UserDashboard({ userId }) {
const results = useQueries({
queries: [
{
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
},
},
{
queryKey: ['posts', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}/posts`);
return res.json();
},
},
{
queryKey: ['followers', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}/followers`);
return res.json();
},
},
],
});
// results is an array: [userResult, postsResult, followersResult]
const [userQuery, postsQuery, followersQuery] = results;
if (userQuery.isLoading) return <Skeleton />;
if (userQuery.isError) return <Error error={userQuery.error} />;
return (
<div>
<h1>{userQuery.data.name}</h1>
<p>Posts: {postsQuery.data?.length || 0}</p>
<p>Followers: {followersQuery.data?.length || 0}</p>
</div>
);
}
All three fetches happen in parallel. The hook returns results in the same order as the query configurations.
When to Use useQueries vs Multiple useQuery Calls
Use useQueries when:
- You need to fetch multiple independent resources (user profile, posts, followers)
- The number of queries is small and fixed (2–5)
- All queries are related to the same page or feature
Use multiple useQuery calls when:
- The number of queries is dynamic or unbounded (list of items, each with its own query)
- Queries are scattered across different components
- Each query has complex custom logic (custom hooks wrapping
useQuery)
For a dynamic list of items, each needing a fetch, use useQueries with a dynamic array:
function UserCommentsList({ userIds }) {
const results = useQueries({
queries: userIds.map(userId => ({
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
},
})),
});
return (
<div>
{results.map((result, idx) => (
<div key={userIds[idx]}>
{result.isLoading && <Skeleton />}
{result.isError && <Error error={result.error} />}
{result.isSuccess && <div>{result.data.name}</div>}
</div>
))}
</div>
);
}
Here, the queries array is generated dynamically from userIds. Each fetch runs in parallel, and results are mapped back to the original IDs.
Combining Results Across Queries
Often you need to compute derived state from multiple queries:
function UserDashboard({ userId }) {
const [userQuery, postsQuery, followersQuery] = useQueries({
queries: [
{
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
},
{
queryKey: ['posts', userId],
queryFn: () => fetch(`/api/users/${userId}/posts`).then(r => r.json()),
},
{
queryKey: ['followers', userId],
queryFn: () => fetch(`/api/users/${userId}/followers`).then(r => r.json()),
},
],
});
// Combine results into a single computed state
const isLoading = userQuery.isLoading || postsQuery.isLoading || followersQuery.isLoading;
const isError = userQuery.isError || postsQuery.isError || followersQuery.isError;
const error = userQuery.error || postsQuery.error || followersQuery.error;
if (isLoading) return <Skeleton />;
if (isError) return <Error error={error} />;
const stats = {
name: userQuery.data.name,
postCount: postsQuery.data.length,
followerCount: followersQuery.data.length,
};
return <Dashboard stats={stats} />;
}
Or use a custom hook to encapsulate the combination logic:
function useUserDashboardData(userId) {
const [userQuery, postsQuery, followersQuery] = useQueries({
queries: [
{
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
},
{
queryKey: ['posts', userId],
queryFn: () => fetch(`/api/users/${userId}/posts`).then(r => r.json()),
},
{
queryKey: ['followers', userId],
queryFn: () => fetch(`/api/users/${userId}/followers`).then(r => r.json()),
},
],
});
return {
data: {
user: userQuery.data,
posts: postsQuery.data,
followers: followersQuery.data,
},
isLoading: userQuery.isLoading || postsQuery.isLoading || followersQuery.isLoading,
isError: userQuery.isError || postsQuery.isError || followersQuery.isError,
error: userQuery.error || postsQuery.error || followersQuery.error,
};
}
function UserDashboard({ userId }) {
const { data, isLoading, isError, error } = useUserDashboardData(userId);
if (isLoading) return <Skeleton />;
if (isError) return <Error error={error} />;
return <Dashboard stats={data} />;
}
Partial Failures and Progressive Loading
When some queries succeed and others fail, show the successful data while indicating errors:
function UserDashboard({ userId }) {
const [userQuery, postsQuery, followersQuery] = useQueries({
queries: [
{ queryKey: ['user', userId], queryFn: fetchUser },
{ queryKey: ['posts', userId], queryFn: fetchPosts },
{ queryKey: ['followers', userId], queryFn: fetchFollowers },
],
});
// Show whatever data is available
const isAllLoading = userQuery.isPending && postsQuery.isPending && followersQuery.isPending;
const anyError = userQuery.isError || postsQuery.isError || followersQuery.isError;
if (isAllLoading) return <Skeleton />;
return (
<div>
{userQuery.isSuccess && <h1>{userQuery.data.name}</h1>}
{userQuery.isError && <p style={{ color: 'red' }}>Failed to load user</p>}
{postsQuery.isSuccess && <p>Posts: {postsQuery.data.length}</p>}
{postsQuery.isError && <p style={{ color: 'red' }}>Failed to load posts</p>}
{followersQuery.isSuccess && <p>Followers: {followersQuery.data.length}</p>}
{followersQuery.isError && <p style={{ color: 'red' }}>Failed to load followers</p>}
{anyError && <p>Some data failed to load. Retrying...</p>}
</div>
);
}
This pattern is called "progressive loading" or "optimistic rendering": show what you have, indicate what failed, and let the user see partial data while retries happen.
Conditional Queries with useQueries
To skip a query conditionally, use enabled: false:
function UserDashboard({ userId, showFollowers }) {
const [userQuery, postsQuery, followersQuery] = useQueries({
queries: [
{
queryKey: ['user', userId],
queryFn: fetchUser,
},
{
queryKey: ['posts', userId],
queryFn: fetchPosts,
},
{
queryKey: ['followers', userId],
queryFn: fetchFollowers,
enabled: showFollowers, // Skip if not needed
},
],
});
return (
<div>
{userQuery.data && <h1>{userQuery.data.name}</h1>}
{postsQuery.data && <p>Posts: {postsQuery.data.length}</p>}
{showFollowers && followersQuery.data && (
<p>Followers: {followersQuery.data.length}</p>
)}
</div>
);
}
Setting enabled: false prevents the query from running until the condition changes. This is useful for optional features or permission-gated data.
Batch Invalidation After Mutations
Use useQueries in tandem with mutation invalidation:
function useAddFollower(userId) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (targetUserId) => {
const res = await fetch(`/api/users/${userId}/follow/${targetUserId}`, {
method: 'POST',
});
return res.json();
},
onSuccess: () => {
// Invalidate all related queries
queryClient.invalidateQueries({
queryKey: ['followers', userId],
exact: true,
});
queryClient.invalidateQueries({
queryKey: ['user', userId],
exact: true,
});
},
});
}
function UserDashboard({ userId }) {
const [userQuery, followersQuery] = useQueries({
queries: [
{ queryKey: ['user', userId], queryFn: fetchUser },
{ queryKey: ['followers', userId], queryFn: fetchFollowers },
],
});
const { mutate: addFollower } = useAddFollower(userId);
return (
<div>
<h1>{userQuery.data?.name}</h1>
<button onClick={() => addFollower(5)}>Follow User 5</button>
<p>Followers: {followersQuery.data?.length || 0}</p>
</div>
);
}
After a mutation, both the user and followers queries are invalidated in one call, ensuring both are refetched and consistent.
Ordering and Key Dependencies
The order of queries in the array determines the order of results. Keep order stable to avoid bugs:
// Unstable: order changes if userIds changes
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()),
})),
});
// Stable: order is guaranteed
const [user1, user2, user3] = useQueries({
queries: [
{ queryKey: ['user', 1], queryFn: () => fetchUser(1) },
{ queryKey: ['user', 2], queryFn: () => fetchUser(2) },
{ queryKey: ['user', 3], queryFn: () => fetchUser(3) },
],
});
For dynamic lists, map the results back to the original IDs to maintain clarity.
Key Takeaways
useQueriesfetches multiple independent queries in parallel, returning an array of results.- Use
useQueriesfor a small, fixed set of related queries (2–5); use multipleuseQuerycalls or dynamic arrays for unbounded lists. - Combine results into computed state (isLoading, isError) to simplify conditional rendering.
- Show partial results when some queries succeed and others fail, enabling progressive loading UIs.
- Use
enabled: falseto conditionally skip queries based on props or state. - Batch invalidate related queries after mutations to keep cache consistent.
Frequently Asked Questions
How many useQueries should I use in one component?
Ideally, fewer than 5. If you are calling useQueries multiple times in one component or need more than 10 queries, consider breaking the component into smaller children, each calling useQuery independently.
Can I use useQueries to fetch a dynamic list of items?
Yes. Map over the list to generate query configurations:
const results = useQueries({
queries: items.map(item => ({
queryKey: ['item', item.id],
queryFn: () => fetch(`/api/items/${item.id}`).then(r => r.json()),
})),
});
All queries run in parallel. Use combine function (in TanStack Query v5) to compute aggregate state.
What is the combine function in useQueries?
In v5, useQueries supports an optional combine function to aggregate results:
const { data, isLoading } = useQueries({
queries: [...],
combine: (results) => ({
data: results.map(r => r.data),
isLoading: results.some(r => r.isLoading),
}),
});
This replaces manual result aggregation and is more performant.
Do all queries in useQueries run in parallel?
Yes, all queries in a single useQueries call run in parallel. However, TanStack Query may batch HTTP requests if multiple queries are created in the same render cycle, depending on your network library.
Can I use useQueries with mutate operations?
useQueries is for fetching only. For mutations, use useMutation. You can call both in the same component: useQueries for fetching, useMutation for updates.