Guides & Concepts

Parallel Queries

Parallel queries are queries that run at the same time so the UI does not wait for one request before starting the next.

Manual Parallel Queries

When the number of queries is fixed, create multiple query controllers on the same host. They will all subscribe when the host connects.

ts
import { LitElement, html } from 'lit'
import { createQueryController } from '@tanstack/lit-query'

class DashboardView extends LitElement {
  private readonly users = createQueryController(this, {
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  private readonly teams = createQueryController(this, {
    queryKey: ['teams'],
    queryFn: fetchTeams,
  })

  private readonly projects = createQueryController(this, {
    queryKey: ['projects'],
    queryFn: fetchProjects,
  })

  render() {
    const users = this.users()
    const teams = this.teams()
    const projects = this.projects()

    if (users.isPending || teams.isPending || projects.isPending) {
      return html`Loading...`
    }

    if (users.isError || teams.isError || projects.isError) {
      return html`Unable to load dashboard`
    }

    return html`
      <dashboard-summary
        .users=${users.data}
        .teams=${teams.data}
        .projects=${projects.data}
      ></dashboard-summary>
    `
  }
}
import { LitElement, html } from 'lit'
import { createQueryController } from '@tanstack/lit-query'

class DashboardView extends LitElement {
  private readonly users = createQueryController(this, {
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  private readonly teams = createQueryController(this, {
    queryKey: ['teams'],
    queryFn: fetchTeams,
  })

  private readonly projects = createQueryController(this, {
    queryKey: ['projects'],
    queryFn: fetchProjects,
  })

  render() {
    const users = this.users()
    const teams = this.teams()
    const projects = this.projects()

    if (users.isPending || teams.isPending || projects.isPending) {
      return html`Loading...`
    }

    if (users.isError || teams.isError || projects.isError) {
      return html`Unable to load dashboard`
    }

    return html`
      <dashboard-summary
        .users=${users.data}
        .teams=${teams.data}
        .projects=${projects.data}
      ></dashboard-summary>
    `
  }
}

Each controller receives the same ReactiveControllerHost. If no explicit QueryClient is passed, each controller resolves the nearest connected QueryClientProvider.

Dynamic Parallel Queries

When the number of queries changes with host state, use createQueriesController. It accepts a queries array and returns an accessor for the array of query results.

Use an options getter when the query list depends on reactive host fields:

ts
import { LitElement, html } from 'lit'
import { createQueriesController } from '@tanstack/lit-query'

class UsersDetails extends LitElement {
  static properties = {
    userIds: { attribute: false },
  }

  userIds: Array<string> = []

  private readonly users = createQueriesController(this, () => ({
    queries: this.userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUserById(id),
    })),
  }))

  render() {
    const userQueries = this.users()

    return html`
      <ul>
        ${userQueries.map((query, index) => {
          if (query.isPending) return html`<li>Loading...</li>`
          if (query.isError) return html`<li>Error loading user</li>`

          return html`<li>${this.userIds[index]}: ${query.data.name}</li>`
        })}
      </ul>
    `
  }
}
import { LitElement, html } from 'lit'
import { createQueriesController } from '@tanstack/lit-query'

class UsersDetails extends LitElement {
  static properties = {
    userIds: { attribute: false },
  }

  userIds: Array<string> = []

  private readonly users = createQueriesController(this, () => ({
    queries: this.userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUserById(id),
    })),
  }))

  render() {
    const userQueries = this.users()

    return html`
      <ul>
        ${userQueries.map((query, index) => {
          if (query.isPending) return html`<li>Loading...</li>`
          if (query.isError) return html`<li>Error loading user</li>`

          return html`<li>${this.userIds[index]}: ${query.data.name}</li>`
        })}
      </ul>
    `
  }
}

The order of the results matches the order of the input queries.

Combining Results

Use combine when a component wants one derived value instead of an array of query results:

ts
private readonly dashboard = createQueriesController(this, {
  queries: [
    { queryKey: ['stats'], queryFn: fetchStats },
    { queryKey: ['projects'], queryFn: fetchProjects },
  ],
  combine: ([stats, projects]) => ({
    activeUsers: stats.data?.activeUsers ?? 0,
    projects: projects.data ?? [],
    isPending: stats.isPending || projects.isPending,
    isError: stats.isError || projects.isError,
  }),
})
private readonly dashboard = createQueriesController(this, {
  queries: [
    { queryKey: ['stats'], queryFn: fetchStats },
    { queryKey: ['projects'], queryFn: fetchProjects },
  ],
  combine: ([stats, projects]) => ({
    activeUsers: stats.data?.activeUsers ?? 0,
    projects: projects.data ?? [],
    isPending: stats.isPending || projects.isPending,
    isError: stats.isError || projects.isError,
  }),
})
ts
render() {
  const dashboard = this.dashboard()

  if (dashboard.isPending) return html`Loading...`
  if (dashboard.isError) return html`Unable to load dashboard`

  return html`
    <p>Total projects: ${dashboard.projects.length}</p>
    <p>Active users: ${dashboard.activeUsers}</p>
  `
}
render() {
  const dashboard = this.dashboard()

  if (dashboard.isPending) return html`Loading...`
  if (dashboard.isError) return html`Unable to load dashboard`

  return html`
    <p>Total projects: ${dashboard.projects.length}</p>
    <p>Active users: ${dashboard.activeUsers}</p>
  `
}

Having the same query key more than once in the queries array can cause those entries to share cached data. Deduplicate repeated keys first if each rendered row needs independent query state.