Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: useQueries for executing a dynamic number of queries in the same component #2126

Closed
ephys opened this issue Dec 6, 2021 · 6 comments
Closed
Labels
future 🔮 An enhancement or feature proposal that will be addressed after the next release

Comments

@ephys
Copy link

ephys commented Dec 6, 2021

Summary

This is yet another request that relates to pagination.

As far as I know, there are currently 3 ways to handle pagination, each with their disadvantages.

  • The "one component per page" method. Works for simple cases but doesn't work if you need to sort the data from multiple pages. (Like for inserting the new data from a subscription).
  • The "one useQuery with changing variables + useEffect to keeps the previous results from useQuery". Already better for complex cases. But can only restore the first page from the cache. (and if I'm not mistaken, cache updates on items from that list are not reflected).
  • The graphcache way. Also works for more complex scenarios, and includes subsequent pages, but if a resource was deleted, its page won't be updated and stale data will stay until the actual page is refreshed (unless there is a persistent cache installed in which case it's just there forever).

Proposed Solution

A new useQueries hook would provide the building block for this scenario. This hook would accept an array of UseQueryArgs, and return an array of UseQueryResponse.

If you need to load a new page, you simply push a new entry to the array that is passed to useQueries.

@ephys ephys added the future 🔮 An enhancement or feature proposal that will be addressed after the next release label Dec 6, 2021
@JoviDeCroock
Copy link
Collaborator

The problem here is the deterministic aspect, for instance we could compose this out of multiple useQuery calls but that's against the rules of hooks so the only other option we have is to open a set of streams which could get a bit of akward when updates come in.

Generally this RFC seems incomplete as for instance the graphCache way is a general problem when there is no updater specified, when we have an updater that calls invalidate on the node that gets deleted (we can't automatically derive which node got deleted) then the rerender should occur, is this not happening?

@ephys
Copy link
Author

ephys commented Dec 9, 2021

Thanks for the response. I'll try to explain my issue with graphCache's relayPagination a little bit better :)

My issue is an interaction between the cache-and-network policy and relayPagination, exacerbated by offlineExchange.

I have the following scenario:

  • User A opens a page with an infinite scroll (backed by relayPagination) and loads the first 2 pages.
  • The loaded data is put in the offline cache.
  • User B deletes an item that was located on page 2. As it's a different device, calling invalidate only invalidates it from User B's cache and not User A.
  • User A opens the same page again, the first two pages are retrieved from their cache and displayed. cache-and-network policy causes the first page to refresh, but not the second one. The deleted item is therefore still present. Calling the revalidate function doesn't help either as it only refreshes the first page too.

Here, User A is stuck with a deleted item and refreshing the page won't help because of the offlineExchange.


With something like useQueries, I have manual control over every query that has occured for a given page. I can decide to refresh each of them individually, to display the only first page again once the user comes back. I can choose what I display based on whether the user is coming back to a page, or whether it's a new history entry.


The rfc is currently half baked. I've only started looking into how / if useQueries could be implemented.
I do think it's interesting to look into though. (unless I've wildly misused graphcache and my above problem can be solved).

@JoviDeCroock
Copy link
Collaborator

That is an interesting scenario, I have to admit, normalized caching and lists are famously hard 😅 I would probably resort to invalidating the queries at hand by a mechanism like const toInvalidate = cache.inspectFields('Query').filter(x => x.fieldName === 'yourList' and call invalidate on these like here.

I do realise that useQueries here could help because you can call reexecute on all of these but I feel like the above ends up in the same thing without "polluting" your front-end code.

@kitten
Copy link
Member

kitten commented Dec 9, 2021

I think ultimately we'll have to bring a first class solution to Graphcache for this, possibly, because "trains of invalidation", where a single update triggers more when writing to the cache, or validation due to refetches isn't a thing right now

@ephys
Copy link
Author

ephys commented Dec 9, 2021

Invalidating each query individually would work for a force refresh.

One of the things I like about this RFC that I'm not sure can currently be done is that I would like to display different pages when the user is coming back to a history entry (back button) VS creating a new history entry:

If the user presses back and arrives back on an infinite list, I want to restore the state of that list to exactly what the user had so I can properly restore their scroll. I'd do that by storing used page variables in history.state and restore each page with cache-first.

But for new history entries, I'd only display the first page with cache-and-network as displaying only the first page is enough and I want to refresh it.

In both cases I don't want to refresh every page automatically (too expensive). At most, the first one.

@kitten
Copy link
Member

kitten commented Mar 19, 2023

Sorry for coming back to this late.
I have to be honest, balancing the needs of keeping our bindings lean and maintainable and making infinite scrolling easy has been hard in the past. However, I'm still convinced that our recommended approach is best in most cases.

While it doesn't work for React Native's FlatList (which has to use a Graphcache resolver essentially to get a single array of edges), I think the approach of rendering a set of "infinitely scrolling" pages as a set of child components that all return a fragment of items is more scalable.
Both for us to maintain, and — from personal experience — in user code.

While I do see this as a UI concern, and useQueries doesn't conflict with this opinion, I think maintaining a "multiplex query binding" for each UI framework will be tricky, and I wouldn't be able to even come up with an API that can sustain such a complex requirement in React specifically.

While it's a tricky question, and it pains me that we haven't had time to expand on these docs for this specific issue, I'd still point at this approach today: https://formidable.com/open-source/urql/docs/basics/ui-patterns/#infinite-scrolling
and furthermore, would like to say that I truly believe it's "good enough" for many apps that need infinite scrolling.

In short, rendering multiple components that all are responsible for one page each, rendering fragments, is a very powerful and flexible approach, and still allows invalidation, since input parameters to useQuery can be changed per page, and the "managing" component can still maintain an abstract state of how many pages to render.

@kitten kitten closed this as not planned Won't fix, can't repro, duplicate, stale Mar 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
future 🔮 An enhancement or feature proposal that will be addressed after the next release
Projects
None yet
Development

No branches or pull requests

3 participants