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:
query { u1: user(id: ...) u2: user(id: ...) ... } - I don't do that in my app and I discourage other colleagues from doing thatExample:
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.
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"
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:
In the above example I've hardcoded it which is achievable with a static number of hooks; however, if
numPreloadedPagesbecomes dynamic it's not possible as per my understanding of hooks.Example: