redux-toolkit icon indicating copy to clipboard operation
redux-toolkit copied to clipboard

RTKQ - Hook for array of queries

Open nicolas-chaulet opened this issue 2 years ago • 9 comments

Hi All! I was wondering if there was a good way to handle an array of queries that change from one render to the next? This issue on react-query describes the problem quite well: https://github.com/tannerlinsley/react-query/issues/138 this resulted in the new hook useQueries: https://react-query.tanstack.com/reference/useQueries

This seems to be a very natural use case and until now I worked around it with a custom queryFn but of course caching does not behave since ideally I would have a cached value for every individual object instead of a value for the request array.

I could not find something that would allow us to do what is achieved by react-query with the useQueries hook, any insight?

nicolas-chaulet avatar Jul 25 '21 17:07 nicolas-chaulet

It's pretty much possible on the Redux side but you'd have to write your own hook for it at the moment. The current hook implementation is here: https://github.com/reduxjs/redux-toolkit/blob/ed75ee07c4116ea22cb69a52d55acee5c4991054/packages/toolkit/src/query/react/buildHooks.ts#L493

I guess we'll consider it in a future release, but right now there are higher prioritized items on the list.

phryneas avatar Jul 25 '21 21:07 phryneas

Thanks for the pointer! However I am not sure this would give me the desired caching behaviour. For example if I first request objects with ids [1,2,3] and then later on objects with ids [3,4,5] I would like object 3 to come from the cache. I'll probably use the entity adapter for now that allows this use case quite nicely, just was curious if there was something I was missing with RTKQ.

nicolas-chaulet avatar Jul 26 '21 13:07 nicolas-chaulet

For example if I first request objects with ids [1,2,3] and then later on objects with ids [3,4,5] I would like object 3 to come from the cache.

Assuming you mean three requests for 1,2,3 each and later three requests for 3,4,5 each, that's not a problem - initiate might fire, but it would not make a request since there is already data in the cache.

phryneas avatar Jul 26 '21 13:07 phryneas

Yes thanks, makes total sense, I was indeed missing something 😄

nicolas-chaulet avatar Jul 26 '21 13:07 nicolas-chaulet

@nicolas-chaulet Did you manage to work something out? I have similar needs for a project of my own, but haven't been successful in creating a custom hook for this yet...

Jonesus avatar Aug 02 '21 13:08 Jonesus

Hey! I ended up implementing my own slice starting from an entity adapter, I can easily normalise my data and that was actually quite simple to implement.

nicolas-chaulet avatar Aug 02 '21 14:08 nicolas-chaulet

For anyone coming to this issue I made a hook for a single query. I don't know if this is can generalized to work with any query.

// useGetCurrencyRateMultipleQueries.ts

export const useGetCurrencyRateMultipleQueries = (
  options: (GetCurrencyRatesOptions | SkipToken)[]
) => {
  const dispatch = useAppDispatch();

  useEffect(() => {
    const subscriptions = options
      .filter((options): options is GetCurrencyRatesOptions => {
        return options !== skipToken;
      })
      .map((options) => {
        return dispatch(exopenApi.endpoints.getCurrencyRates.initiate(options));
      });
    return () => {
      for (const subscription of subscriptions) {
        subscription.unsubscribe();
      }
    };
  }, [dispatch, options]);

  return useAppSelector((state) => {
    return options.map((options) => {
      return exopenApi.endpoints.getCurrencyRates.select(options)(state);
    });
  });
};
const options = useMemo(() => {
    return filteredExchangeStrategies.map((strategy) => {
      if (!exchangeStrategies) {
        return skipToken;
      }
      return {
        from: strategy.fromCurrency,
        to: exchangeStrategies.toCurrency,
        periods: [periodString],
        provider: strategy.rateProvider,
        type: strategy.rateType,
      };
    });
  }, [exchangeStrategies, filteredExchangeStrategies, periodString]);

  const currencyRates = useGetCurrencyRateMultipleQueries(options);

hornta avatar Jun 09 '22 14:06 hornta

I tried my hand at generalizing @hornta's solution, but for type safety it seemed to require some pretty dirty imports, maybe someone knows how to clean the type signatures a bit?

import { ThunkDispatch } from '@reduxjs/toolkit';
import { QueryDefinition } from '@reduxjs/toolkit/dist/query';
import { QueryActionCreatorResult } from '@reduxjs/toolkit/dist/query/core/buildInitiate';
import { ApiEndpointQuery } from '@reduxjs/toolkit/dist/query/core/module';

export function useQueries<QueryArgs, ReturnValue>(
  endpointQuery: ApiEndpointQuery<
    QueryDefinition<QueryArgs, any, any, ReturnValue, string>,
    any
  >
) {
  return function (options: (QueryArgs | SkipToken)[]) {
    const dispatch = useAppDispatch<ThunkDispatch<any, any, AnyAction>>();

    useEffect(() => {
      const subscriptions: QueryActionCreatorResult<any>[] = options
        .filter((options): options is QueryArgs => options !== skipToken)
        .map((options) => dispatch(endpointQuery.initiate(options)));

      return () => {
        subscriptions.forEach((s) => s.unsubscribe());
      };
    }, [dispatch, options]);

    return useAppSelector((state) =>
      options.map((options) => endpointQuery.select(options)(state))
    );
  };
}

But with this you can easily define more than one multi-query with properly inferred types:

const useGetDogsQueries = useQueries(api.endpoints.getDogs);
const useGetCatsQueries = useQueries(otherApi.endpoints.getCats);


const dogs = useGetDogsQueries(options);
const cats = useGetCatsQueries(options);

Jonesus avatar Jun 20 '22 12:06 Jonesus

@Jonesus In this discussion phryneas linked to a PR with an initial implementation of a multi query hook https://github.com/reduxjs/redux-toolkit/discussions/2398#discussioncomment-2925776

I've not yet tested that branch but maybe you can look into that as well and see if you can help.

hornta avatar Jun 20 '22 12:06 hornta