Back to Ragflow

parallel-use-queries: Use useQueries for Dynamic Parallel Queries

web/.agents/skills/tanstack-query-best-practices/rules/parallel-use-queries.md

0.25.13.8 KB
Original Source

parallel-use-queries: Use useQueries for Dynamic Parallel Queries

Priority: MEDIUM

Explanation

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.

Bad Example

tsx
// 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),
  }))
}

Good Example

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

Good Example: With Combine Option

tsx
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} />
}

Good Example: Dependent Parallel Queries

tsx
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...
}

Good Example: With Suspense

tsx
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} />
}

Context

  • Queries run in parallel, not sequentially
  • Each query is cached independently
  • Use combine to transform results array into single value
  • Empty queries array is valid (returns empty results)
  • Pairs well with useSuspenseQueries for guaranteed data
  • Individual query options (staleTime, etc.) apply per-query