swr icon indicating copy to clipboard operation
swr copied to clipboard

Missing an equivalent of React Query's useQueries hook

Open gfox1984 opened this issue 4 years ago • 14 comments

There does not seem to be an equivalent of the React Query's useQueries hook in SWR.

Being able to run a variable number of queries in parallel using SWR and yet benefit from caching per individual items would be a big enabler. I know that I can use a custom fetcher with SWR which returns the Promise.all of multiple requests, but that doesn't cache results individually.

A good use case is given on the link above. Imagine having to fetch multiple users at once, and then in other parts of the code, only request for one of these users. You would definitely like to avoid making a new request for the same user requested earlier.

Note: I tried to ask the community for a way to do that with SWR, without success.

gfox1984 avatar Mar 16 '21 09:03 gfox1984

Thanks for the detailed description! So the solution would be something similar to the impl. of useSWRInfinite, but requests are fired together instead of paginated.

Happy to add it if someone comes up with a PR, I think useSWRList would be a nice name. 👍

shuding avatar Mar 17 '21 21:03 shuding

I think we could use the global mutate function to cache the each item based on its key.

import { mutate } from 'swr'
function useSWRList(queryList) {
  const mutations = queryList.map(({ queryKey, queryFn }) => {
    return mutate(queryKey, () => queryFn(...queryKey))
  })

  return useSWR(
    queryList.map((v) => v.queryKey),
    () => Promise.all(mutations)
  )
}

FYI: https://codesandbox.io/s/swr-list-xw58t

promer94 avatar Mar 19 '21 09:03 promer94

We'd also want to catch errors for each key individually, so that some calls can fire even if others aren't ready. If the first request's key throws an error, the others should still fire.

nandorojo avatar Mar 19 '21 13:03 nandorojo

@nandorojo Hi, would you mind check the codesandbox again. I have updated the example.

const mutations = queryList.map(({ queryKey, queryFn }) => () =>
    mutate(queryKey, () => queryFn(...queryKey))
      // The data has been saved in cache based on key
      .then((v) => v)
      // The error has been save in cache base on key
      // so the error was handled individually
      .catch((e) => ({
        error: e,
        key: queryKey
      }))
  )

I feel like this pattern might be batter then useQueries hook. It only rerenders onces when all data resolves. useQueries hook will rerender much more times than useSWRList which might cause unexpected layout shift or bad performance

promer94 avatar Mar 30 '21 03:03 promer94

@promer94 Hi, thanks for the solution. It seems resolving the case.

Just to keep in consideration for the PR, in my case I didn't need to mutate the entire list but just fetch every time the array changes and a new request should be performed.

So in my case I modified the function like this

import useSWR, { mutate, cache } from 'swr'

export function useSWRList(queryList, queryFn) {
  const mutations = queryList.map((queryKey) => () => {
    const cachedResult = cache.get(queryKey)

    if (cachedResult) {
      return cachedResult
    }

    return (
      mutate(queryKey, () => queryFn(queryKey))
      // The error has been save in cache base on key
      // so the error was handled individually
      .catch((e) => ({
        error: e,
        key: queryKey,
      }))
    )
  })

  return useSWR(
    queryList,
    () => Promise.all(mutations.map((v) => v()))
  )
}

eliascotto avatar Apr 13 '21 07:04 eliascotto

Would love to see this!

This is useful for any situation where you have lists that contain elements that may be shared across parents. For example imagine looking up portfolios of stocks (eg. an array of tickers). There's a chance for overlap between two portfolios (eg. they both contain AAPL), so you want to cache the individual stock queries.

This type of use case can't be replicated with sub-components and the normal useSWR hook. You could do that if all you wanted to do was show a profile for each stock. But then you wouldn't be able to calculate aggregate statistics like "net worth" using the entire portfolio of holdings.

ianstormtaylor avatar Sep 21 '21 18:09 ianstormtaylor

Is this currently being worked on? We could use this & would be interested in picking this up.

mAAdhaTTah avatar Nov 05 '21 04:11 mAAdhaTTah

I'm looking for this feature too!! @elias94 Is your solution updated to useSWR 1.0.0 ( I think the cache is imported from useSWRConfig)

rodbs avatar Nov 05 '21 17:11 rodbs

Hi @promer94 thanks for the response and the examples above. However, like many above we would love to have the ability to load data from multiple requests as data comes back, in order words, rerender anytime we receive a response from one of the requests.

To respond to your reply above:

I feel like this pattern might be batter then useQueries hook. It only rerenders onces when all data resolves. useQueries hook will rerender much more times than useSWRList which might cause unexpected layout shift or bad performance

We have optimized UX to handle layout and component caching for performance. Showing some data as soon as they're available has been proven to help with attracting users' attention and retention. I feel like dealing with layout and rerender performance should be something developers can take into consideration themselves.

We would love to have useQueries equivalent in addition to the Promise.all pattern you've shared above to provide best of both worlds~ What do you think?

wei avatar Nov 11 '21 06:11 wei

Any status update on this?

Totalbug92 avatar Nov 24 '21 13:11 Totalbug92

I started working on it initially before getting pulled away to other things. I also realize, based on convos here, that the desired semantics of this hook aren't necessarily clear. I'll probably just pick an approach to get it into a PR for discussion, but I don't really have so much code written that, should someone else have the time, they wouldn't/shouldn't pick this up.

mAAdhaTTah avatar Nov 24 '21 14:11 mAAdhaTTah

desired semantics of this hook aren't necessarily clear

Agreed here @mAAdhaTTah, I think the most important thing to do first is getting a RFC/proposal instead of jumping into the implementation directly.

shuding avatar Dec 01 '21 02:12 shuding

I think the project should aim to mimic the interface that useQueries provides, so that each of the results are loaded independently. It’s possible to create a user-land implementation for the variant which works in an all-or-nothing fashion, but independent loading of each query doesn’t seem possible without mimicking huge parts of SWR, due to the fact that a { data, error, isValidating } object should be managed for each individual query.

Having the dependency collection optimization in mind, I would suggest the useSWRList hook to return with an object of array values:

const { data, error, isValidating } = useSWRList(["a", "b", "c"], config);
// data[i]
// error[i]
// isValidating[i]

rather than an array of objects:

const results = useSWRList(["a", "b"], config);
// results[i].data
// results[i].error
// results[i].isValidating

because when fetching each item on the list, chances are that all of the given items will be used – the access of data feels easier to track than result[i].data.

However, error and isValidating may not be used for any elements at all, so when an error happens for an item, no re-render may be necessary, depending on the renderer code.

Also, I think the useSWRList hook should accept a single optional config (and fetcher) parameter, as:

  • items loaded with useSWRList should have identical types
  • the request for multiple items should possibly (but not necessarily) be sent to the same endpoint, but with different parameters

I would recommend suggesting developers to use the useSWRList hook only to load a variable amount of items of the same kind – for use-cases other than that, use individual useSWR hooks or multiple useSWRList calls grouped by the type of items requested.

kripod avatar Jan 13 '22 20:01 kripod

@shuding and anyone else in this issue: I've opened an RFC for this hook. I'm also willing to implement it once we agree on the intended semantics. I was doing some refactoring of our code and need this hook to fix some things so I finished up the RFC I started a while back. Also happy to implement this once we align on semantics.

Let me know what y'all think!

mAAdhaTTah avatar May 21 '22 13:05 mAAdhaTTah