Parallel queries when working with React Query

2 min read

The Simple Approach: Multiple useQuery calls
This is the most straightforward when you need independent data in the same component:
function Dashboard() {
const { data: repos, isLoading: reposLoading } = useQuery({
queryKey: ['repos'],
queryFn: fetchRepos
})
const { data: members, isLoading: membersLoading } = useQuery({
queryKey: ['members'],
queryFn: fetchMembers
})
// Each loads independently - great UX!
if (reposLoading) return <div>Loading repos...</div>
if (membersLoading) return <div>Loading members...</div>
return (
<div>
<ReposList repos={repos} />
<MembersList members={members} />
</div>
)
}
useQueries for Dynamic Parallel Fetching
This is where it becomes really useful when you need to get data for an unknown number of items:
function useRepoIssues(repos) {
return useQueries({
queries: repos?.map(repo => ({
queryKey: ['repos', repo.name, 'issues'],
queryFn: () => fetchIssues(repo.name)
})) ?? []
})
}
function ReposDashboard() {
const { data: repos } = useQuery({
queryKey: ['repos'],
queryFn: fetchRepos
})
const issuesQueries = useRepoIssues(repos)
// Derive total issues across all repos
const totalIssues = issuesQueries
.map(query => query.data?.length ?? 0)
.reduce((sum, count) => sum + count, 0)
return (
<div>
<h2>Total Issues: {totalIssues}</h2>
{repos?.map((repo, index) => (
<RepoCard
key={repo.id}
repo={repo}
issues={issuesQueries[index]?.data}
isLoading={issuesQueries[index]?.isLoading}
/>
))}
</div>
)
}
Shareable Query Options Pattern
// queries.js
export const repoOptions = {
queryKey: ['repos'],
queryFn: fetchRepos,
staleTime: 5 * 60 * 1000 // 5 minutes
}
export const membersOptions = {
queryKey: ['members'],
queryFn: fetchMembers,
staleTime: 10 * 60 * 1000 // 10 minutes
}
// hooks.js
export const useRepos = () => useQuery(repoOptions)
export const useMembers = () => useQuery(membersOptions)
export const useReposAndMembers = () => useQueries({
queries: [repoOptions, membersOptions]
})
Real-World Example: User Profile Page
function UserProfile({ userId }) {
const queries = useQueries({
queries: [
{
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
},
{
queryKey: ['user', userId, 'posts'],
queryFn: () => fetchUserPosts(userId)
},
{
queryKey: ['user', userId, 'followers'],
queryFn: () => fetchUserFollowers(userId)
}
]
})
const [user, posts, followers] = queries
const isLoading = queries.some(q => q.isLoading)
const hasError = queries.some(q => q.isError)
if (isLoading) return <ProfileSkeleton />
if (hasError) return <ErrorMessage />
return (
<div>
<UserInfo user={user.data} />
<PostsList posts={posts.data} />
<FollowersList followers={followers.data} />
</div>
)
}
The main point is that parallel queries offer the best of both worlds. They allow for separate caching and management while also providing coordinated loading states when needed. This is much better than using Promise.all, which ties everything together!
0
Subscribe to my newsletter
Read articles from Tiger Abrodi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Tiger Abrodi
Tiger Abrodi
Just a guy who loves to write code and watch anime.