Apollo-feature-requests: useQueries hook to execute variable number of queries in one hook

Created on 27 Sep 2019  路  2Comments  路  Source: apollographql/apollo-feature-requests

My component receives an array (of variable length) of ids and I need to execute a query for each of them. (I cannot change the schema so I really need to execute 1 query per id).

Because of the way hooks work I cannot call useQuery in a loop, so my options are:

  • create a query dynamically, such as query { u1: user(id: ...) u2: user(id: ...) ... } - I don't do that in my app and I discourage other colleagues from doing that
  • use HOC or render props pattern - I'm trying to avoid those in favour of hooks, mainly because I noticed that programmers without functional programming experience have trouble understanding them
  • create my own hook that would basically duplicate useQuery behaviour, but for multiple queries

Example:

const [{ data: d1, loading: l1, errors: e1 }, { data: d2, loading: l2, errors: e2 }] = 
  useQueries({ query: ..., variables: ... }, { query: ..., variables: ... })

Example for variable number of queries (this is basically what I need to do)

const queries = users.map(user => ({ query: ..., variables: { id: user.id } });
const results = useQueries(queries);
const loading = some('loading', results); // import { some } from lodash/fp;

useQuery would then be a special case of useQueries.
Alternatively useQuery signature could change to accept also an array of queries.
In any case, current behaviour would not change.

I couldn't find any other people trying to run variable number of queries using hooks,
so I'm not sure how useful this change would be for others. Anyway currently I need
to resort to HOCs for this use case, which I would like to avoid.

Most helpful comment

In my situation, we are paginating a data-table and preloading a variable number of next pages after the current page.

For example, if user visits page 0, we preload page 1 and 2 after the initial query resolves.

First ideas of API would be something like this:

const { data, loading, error } = useQuery(LIST, { variables: { limit: 10, offset: 0 } })
useManyQueries([
  [LIST, { skip: !data, variables: { limit: 10, offset: 1 } }],
  [LIST, { skip: !data, variables: { limit: 10, offset: 2 } }],
])

In the above example I've hardcoded it which is achievable with a static number of hooks; however, if numPreloadedPages becomes dynamic it's not possible as per my understanding of hooks.

Example:

const currentPage = 0
const limit = 10
const numPreloadedPages = 4
const { data, loading, error } = useQuery(LIST, { variables: { limit, offset: currentPage * limit } })

const preloadPages = Array.from(new Array(numPreloadedPages)).map((_, i) => i + currentPage + 1)
// => [1, 2, 3, 4]

useManyQueries(preloadPages.map((page) => [
  LIST, { skip: !data, variables: { limit, offset: page * limit } }
]))

All 2 comments

In my situation, we are paginating a data-table and preloading a variable number of next pages after the current page.

For example, if user visits page 0, we preload page 1 and 2 after the initial query resolves.

First ideas of API would be something like this:

const { data, loading, error } = useQuery(LIST, { variables: { limit: 10, offset: 0 } })
useManyQueries([
  [LIST, { skip: !data, variables: { limit: 10, offset: 1 } }],
  [LIST, { skip: !data, variables: { limit: 10, offset: 2 } }],
])

In the above example I've hardcoded it which is achievable with a static number of hooks; however, if numPreloadedPages becomes dynamic it's not possible as per my understanding of hooks.

Example:

const currentPage = 0
const limit = 10
const numPreloadedPages = 4
const { data, loading, error } = useQuery(LIST, { variables: { limit, offset: currentPage * limit } })

const preloadPages = Array.from(new Array(numPreloadedPages)).map((_, i) => i + currentPage + 1)
// => [1, 2, 3, 4]

useManyQueries(preloadPages.map((page) => [
  LIST, { skip: !data, variables: { limit, offset: page * limit } }
]))

in case anyone is still looking at this here's something I pulled out of my butt when I needed this:

import { useApolloClient } from "@apollo/client";
import { QueryData } from "@apollo/client/react/data";
import { equal } from "@wry/equality";
import * as R from "ramda";
import { useEffect, useReducer, useRef } from "react";

function useDeepMemo(memoFn, key) {
  const ref = useRef();

  if (!ref.current || !equal(key, ref.current.key)) ref.current = { key, value: memoFn() };

  return ref.current.value;
}
const toKey = queryOptions => JSON.stringify(queryOptions);
export function useQueries(queriesOptions, options) {
  const context = { client: useApolloClient() };
  const [tick, forceUpdate] = useReducer(x => x + 1, 0);

  // This is somewhat leaky, it will keep QueryData instances
  // alive for queries that are no longer passed.
  const queryDatasRef = useRef({});
  const queryDatas = queriesOptions.reduce((map, queryOptions) => {
    const key = toKey(queryOptions);
    const updatedOptions = { ...queryOptions, ...options };
    const queryData =
      queryDatasRef.current[key] ||
      new QueryData({
        options: updatedOptions,
        context,
        onNewData() {
          if (!queryData.ssrInitiated()) {
            // When new data is received from the `QueryData` object, we want to
            // force a re-render to make sure the new data is displayed. We can't
            // force that re-render if we're already rendering however so to be
            // safe we'll trigger the re-render in a microtask.
            Promise.resolve().then(forceUpdate);
          } else {
            // If we're rendering on the server side we can force an update at
            // any point.
            forceUpdate();
          }
        },
      });
    queryData.setOptions(updatedOptions);
    queryData.context = context;

    // SSR won't trigger the effect hook below that stores the current
    // `QueryData` instance for future renders, so we'll handle that here if
    // the current render is happening on the server side.
    if (queryData.ssrInitiated() && !queryDatasRef.current[key])
      queryDatasRef.current[key] = queryData;

    map[key] = queryData;
    return map;
  }, {});

  // `onError` and `onCompleted` callback functions will not always have a
  // stable identity, so we'll exclude them from the memoization key to
  // prevent `afterExecute` from being triggered un-necessarily.
  const memo = {
    queriesOptions,
    options: {
      ...options,
      onError: undefined,
      onCompleted: undefined,
    },
    context,
    tick,
  };

  const queryResults = useDeepMemo(
    () => Object.values(queryDatas).map(queryData => queryData.execute()),
    memo,
  );

  useEffect(() => {
    // We only need one instance of the `QueryData` class for each query, so we'll store it
    // as a ref to make it available on subsequent renders.
    Object.keys(queryDatas).forEach(key => {
      if (!queryDatasRef.current[key]) queryDatasRef.current[key] = queryDatas[key];
    });

    return () => Object.keys(queryDatas).forEach(key => queryDatas[key].cleanup());
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const unmountDep = JSON.stringify(
    queryResults.map(R.pick(["loading", "networkStatus", "error", "data"])),
  );
  useEffect(() => {
    const unmounts = Object.values(queryDatas).map(queryData => queryData.afterExecute());
    return () => unmounts.forEach(unmount => unmount());
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [unmountDep]);

  return queryResults;
}

use it like

  const graphResults = useQueries(
    graphs.map(graph => ({
      query: GET_INSIGHT,
      variables: { ...graph, interval, from, to },
      /* specific useQuery options */
    })),
    { /* general useQuery options */ },
  );

This is pretty crappy, it doesn't clean up any previous queries until the whole thing dismounts. So I wouldn't recommend it for a large number of fast changing queries, but for the 1-10 I needed this for this has proven to be "OK"

Was this page helpful?
0 / 5 - 0 ratings

Related issues

renganatha10 picture renganatha10  路  6Comments

FredyC picture FredyC  路  9Comments

hwillson picture hwillson  路  8Comments

PowerKiKi picture PowerKiKi  路  4Comments

crubier picture crubier  路  4Comments