# Parallel queries

> Several queries at once — separate reads that run side by side in every call style, QueriesBuilder for a list that changes length or order, and one loading indicator over all of them.

A home screen rarely needs one thing. The dashboard of a smart-home app wants
the rooms and the devices; the energy panel wants one reading per device on
screen, however many that is. Fetched one after the other, each request waits
for the one before it — a waterfall, and a slower screen for no reason.

Queries that do not depend on each other run in parallel, and there is
nothing to set up: each read starts its fetch when it subscribes.

## A fixed number of queries

Read each one. Both reads subscribe in the same build, so both requests are
in flight at once. The same dashboard in each of the [four call
styles](https://dualmeta-gmbh.github.io/query_kit/docs/guides/reading-queries-in-widgets.md):

**context.query**

```dart
class HomeDashboard extends StatelessWidget {
  const HomeDashboard({super.key});

  @override
  Widget build(BuildContext context) {
    // Both reads subscribe in this build, so both requests start now.
    final rooms = context.query(roomsQuery());
    final devices = context.query(devicesQuery());

    return DashboardView(rooms: rooms, devices: devices);
  }
}
```

**QueryBuilder**

```dart
Widget homeDashboard() => QueryBuilder<List<Room>>(
      options: roomsQuery(),
      builder: (context, rooms) => QueryBuilder<List<Device>>(
        options: devicesQuery(),
        builder: (context, devices) =>
            DashboardView(rooms: rooms, devices: devices),
      ),
    );
```

Nesting does not serialise the requests: the outer builder's child — the
inner builder — is built in the same frame, before either answer arrives.

**QueryMixin**

```dart
class _HomeDashboardMixinState extends State<HomeDashboardMixin>
    with QueryMixin {
  @override
  Widget build(BuildContext context) {
    final rooms = watchQuery(roomsQuery());
    final devices = watchQuery(devicesQuery());

    return DashboardView(rooms: rooms, devices: devices);
  }
}
```

**QueryController**

```dart
class _HomeDashboardControllersState extends State<HomeDashboardControllers> {
  late final QueryController<List<Room>, List<Room>> _rooms;
  late final QueryController<List<Device>, List<Device>> _devices;

  @override
  void initState() {
    super.initState();
    final client = QueryClientProvider.read(context);
    _rooms = QueryController.create(client, roomsQuery());
    _devices = QueryController.create(client, devicesQuery());
  }

  @override
  void dispose() {
    _rooms.dispose();
    _devices.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => ListenableBuilder(
        listenable: Listenable.merge(<Listenable>[_rooms, _devices]),
        builder: (context, _) =>
            DashboardView(rooms: _rooms.value, devices: _devices.value),
      );
}
```

Each result settles on its own: the rooms can be on screen while the devices
still load, and one failing does not touch the other. When the screen wants
the two as one value — loading while either loads, an error if either failed
— [combine them](https://dualmeta-gmbh.github.io/query_kit/docs/guides/combining-queries.md).

The showcase's *parallel queries* screen reads three posts side by side.
Opening it shows `fetching=3` before any answer arrives. Switch *Slow post 3*
on and press *Refetch all*: the other two settle while post 3 still fetches,
and the count drops to `fetching=1`. *Refetch post 2* moves only that post's
`fetches`:

Live demo: [Parallel queries](https://dualmeta-gmbh.github.io/query_kit/demo/showcase/#/parallel-queries), running in the browser against an in-memory backend ([source](https://github.com/dualmeta-gmbh/query_kit/tree/main/examples/showcase/lib/features/parallel_queries)). Several queries in one widget, and the global fetching count.

## A list of queries

When the set of queries is data — one reading per device on screen — you
cannot write one read per query in source. `QueriesBuilder` observes a list
that may change length or order:

```dart
QueryObserverOptions<EnergyReading> energyQuery(String deviceId) =>
    QueryObserverOptions(
      queryKey: QueryKey(<Object?>['energy', deviceId]),
      queryFn: (context) => repository.energy(deviceId, signal: context.signal),
    );

// One reading per device on screen — however many that is.
Widget energyPanel(List<String> deviceIds) =>
    QueriesBuilder<EnergyReading, double>(
      queries: <QuerySelectOptions<EnergyReading, double>>[
        for (final id in deviceIds)
          energyQuery(id).withSelect((reading) => reading.watts),
      ],
      builder: (context, results) => switch (results.combine(
        (watts) => watts.fold<double>(0, (sum, each) => sum + each),
      )) {
        CombinedPending() => const Text('Measuring…'),
        CombinedError(:final error) => Text('No reading: $error'),
        CombinedData(:final data) => Text('${data.toStringAsFixed(1)} W now'),
      },
    );
```

- **One description per query.** `energyQuery(id)` is an ordinary options
  function; `withSelect` narrows each reading to its watts, so a refetch that
  brings back the same reading rebuilds nothing.
- **Observers are reused by key and occurrence**, so reordering the list
  starts no requests, and adding an id fetches only the new one.
- **Duplicate keys** share one cache entry while keeping their own observers.
- **Each query fails and settles on its own**; `combine` then turns the list
  into one value, pending until every member has data. See [combining
  queries](https://dualmeta-gmbh.github.io/query_kit/docs/guides/combining-queries.md).

It is homogeneous: one data type per collection, because a Dart `List` has
one element type. For queries of **different** types, read each one as above
and combine the results. The other call styles have the same collection:
`QueriesController(client, queries)` is it as a `ValueListenable`, and
`QueriesObserver` is it without Flutter.

The showcase's *query collections* screen is a list of posts built from a
list of ids. *Reverse* reorders them without a single request; *Duplicate
first* adds a second reader of the same entry (`observers=2`, still one
fetch); *Add missing id* adds a post the server does not have, which fails
alone while its neighbours keep their data:

Live demo: [Query collections](https://dualmeta-gmbh.github.io/query_kit/demo/showcase/#/query-collections), running in the browser against an in-memory backend ([source](https://github.com/dualmeta-gmbh/query_kit/tree/main/examples/showcase/lib/features/query_collections)). A list of queries that grows, shrinks and reorders at runtime.

## One indicator for many queries

Parallel queries each report their own `isFetching`. For a single "syncing"
bar over all of them — whichever screen started them — count them instead:

```dart
// A thin bar under the app bar while any device query is fetching — however
// many there are, and whichever screen started them.
class _DevicesSyncIndicatorState extends State<DevicesSyncIndicator> {
  late final IsFetchingController _fetching = IsFetchingController(
    QueryClientProvider.read(context),
    filters: QueryFilters(queryKey: DeviceKeys.all),
  );

  @override
  void dispose() {
    _fetching.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => ValueListenableBuilder<int>(
        valueListenable: _fetching,
        builder: (context, count, _) => count > 0
            ? const LinearProgressIndicator(minHeight: 2)
            : const SizedBox(height: 2),
      );
}
```

`IsFetchingController` is a `ValueListenable<int>` that notifies only when
the count changes, and the filters narrow it — here to every key under
`['devices']`. `client.isFetching(filters: …)` is the same count as a
one-off snapshot.

## Traps

- **A waterfall by accident.** Reading one query only in the success branch
  of another serialises them. That is right when the second needs the
  first's data — see [dependent queries](https://dualmeta-gmbh.github.io/query_kit/docs/guides/dependent-queries.md) — and a
  needless wait when it does not.
- **A hand-written list of reads for a data-driven set.** When which queries
  run comes from data, give each item its own widget — see [a widget per
  row](https://dualmeta-gmbh.github.io/query_kit/docs/guides/reading-queries-in-widgets.md#a-widget-per-row) — or use `QueriesBuilder`, so the set
  of observers follows the list as it changes.

> **Note: In React Query**
>
> Two `useQuery` calls side by side are two reads here too. `useQueries` is
> `QueriesBuilder` / `QueriesController`, homogeneous by design — a
> heterogeneous tuple is a record of separate reads, combined with
> `(a, b).combine(…)`. `useIsFetching` is `IsFetchingController`. See
> [differences from TanStack Query](https://dualmeta-gmbh.github.io/query_kit/docs/reference/differences-from-tanstack.md).
