web/.agents/skills/tanstack-query-best-practices/rules/parallel-use-queries.md
When you need to fetch multiple queries in parallel where the number or identity of queries is dynamic (e.g., fetching details for a list of IDs), use useQueries. It handles parallel execution and returns an array of query results.
// Sequential fetching with useEffect - waterfall
function UserProfiles({ userIds }: { userIds: string[] }) {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchAll() {
const results = []
for (const id of userIds) {
const user = await fetchUser(id) // Sequential!
results.push(user)
}
setUsers(results)
setLoading(false)
}
fetchAll()
}, [userIds])
// N requests run one after another
}
// Multiple useQuery calls - breaks rules of hooks
function UserProfiles({ userIds }: { userIds: string[] }) {
// Can't call hooks in a loop!
const queries = userIds.map(id => useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
}))
}
import { useQueries } from '@tanstack/react-query'
function UserProfiles({ userIds }: { userIds: string[] }) {
const userQueries = useQueries({
queries: userIds.map(id => ({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000,
})),
})
const isLoading = userQueries.some(q => q.isLoading)
const isError = userQueries.some(q => q.isError)
const users = userQueries.map(q => q.data).filter(Boolean)
if (isLoading) return <Loading />
if (isError) return <Error />
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
function UserProfiles({ userIds }: { userIds: string[] }) {
const { data: users, isPending } = useQueries({
queries: userIds.map(id => ({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
})),
// Combine results into single value
combine: (results) => ({
data: results.map(r => r.data).filter(Boolean),
isPending: results.some(r => r.isPending),
isError: results.some(r => r.isError),
}),
})
if (isPending) return <Loading />
return <UserList users={users} />
}
function PostsWithAuthors({ postIds }: { postIds: string[] }) {
// First: fetch all posts in parallel
const postQueries = useQueries({
queries: postIds.map(id => ({
queryKey: ['posts', id],
queryFn: () => fetchPost(id),
})),
})
const posts = postQueries.map(q => q.data).filter(Boolean)
const authorIds = [...new Set(posts.map(p => p.authorId))]
// Then: fetch all unique authors in parallel
const authorQueries = useQueries({
queries: authorIds.map(id => ({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
enabled: posts.length > 0, // Wait for posts
})),
})
// Combine data...
}
import { useSuspenseQueries } from '@tanstack/react-query'
function UserProfiles({ userIds }: { userIds: string[] }) {
const userQueries = useSuspenseQueries({
queries: userIds.map(id => ({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
})),
})
// All data guaranteed - no loading states needed
const users = userQueries.map(q => q.data)
return <UserList users={users} />
}
combine to transform results array into single valueuseSuspenseQueries for guaranteed data