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

[meta] Support windowed pagination #540

Closed
josephsavona opened this issue Nov 2, 2015 · 30 comments
Closed

[meta] Support windowed pagination #540

josephsavona opened this issue Nov 2, 2015 · 30 comments

Comments

@josephsavona
Copy link
Contributor

Relay's pagination model is optimized for infinite scrolling, in which a view requests an increasing larger number of items. A common requirement is windowed pagination in which the UI shows pages of e.g. 10 items, with support for jumping to the first/previous/next/last page (or to an arbitrary page number in between).

This is currently difficult to implement in Relay (see #466 for a writeup by @faassen).

Challenges include:

  • Allowing both first/after and last/before arguments in the same field so long as values are only provided for one of these pairs. This is currently prevented in babel-relay-plugin; the check should be moved to e.g. GraphQLRange.
  • Providing a way to determine a value for the after or before argument value when jumping to an arbitrary page (more generally, how to do offset based pagination over a cursor-based schema).
  • Ensuring that hasNextPage and hasPreviousPage provide meaningful values - the connection spec currently states that the value of hasNextPage and hasPreviousPage must be returned as false unless the user is paginating in the correct direction, even though there may be previous or next edges.
@josephsavona josephsavona changed the title Support windowed pagination [meta] Support windowed pagination Nov 2, 2015
@dminkovsky
Copy link
Contributor

Thank you for your summary @josephsavona.

Having read #466, the connection specs and docs, what I wonder most is how it might be possible to reconcile these two paging methods while keeping cursors opaque. If cursors are opaque, jumping to arbitrary pages seems impossible.

The thing about the opaque cursor-based approach as I understand it is not just that it reflects the infinite scrolling use-case. Given that Relay came from Facebook—a massive distributed system—I assumed its cursor-based paging is more significantly related to the peculiarities of paging in distributed systems: specifically that skip/limit paging is non-performant in distributed applications. The issues are described in this blog post about MongoDB paging, but basically any distributed DB that I've played with has warned about skip/limit paging for this reason[1]. It might make sense to think about this issue from this angle.

[1] c.f. Elasticsearch paging

@josephsavona
Copy link
Contributor Author

@dminkovsky Yup, we use cursor-based pagination precisely because skip/limit isn't performant in large data sets. Also, skip/limit can return overlapping results if items are added between fetching pages.

One option might be to make connection handling injectable. Something like Relay.injectConnectionHandler(handler) where the handler had methods to read the list of edge IDs given the GraphQL arguments, as as well as methods to add/remove sets of edge IDs along with the arguments used to fetch them. This could be based off the existing GraphQLRange API.

@yuzhi - thoughts?

@taion
Copy link
Contributor

taion commented Nov 10, 2015

I'm much less cool than @yuzhi, but I've been prodding at this a bit and have some thoughts.

I think there's really 3 kinds of common pagination patterns: page number pagination, limit/offset pagination, and cursor pagination. As a reference point, DRF is fairly comprehensive and implements all three (though its cursor-based pagination approach is not directly compatible with Relay's assumptions because it only provides start and end cursors).

Relay already handles cursor pagination just fine, so we don't need to talk too much about it, except mention that most cursor paginated REST APIs actually only provide start and end cursors rather than per-element cursors.

Page number based pagination seems like it'd be really "easy" in some sense to handle in Relay - your queries would take the form of connection(page: $page); this essentially works out-of-the-box right now if you write the query as connection(page: $page, first: $DUMMY). This works just fine for window-based pagination based on explicit pages, and the existing PageInfo is essentially fine.

Limit/offset pagination in this context actually seems very similar to cursor pagination; it seems like essentially the same as cursor pagination, except that (1) the cursors are non-opaque to the client, and (2) the cursors can change underneath the client as records are inserted and removed.

One complexity in both cases is how to handle new elements getting inserted into the collections, but frankly neither method of pagination really deals well with dynamic lists anyway.

Partially, #466 I think just speaks to the difficulties of trying to do window-based pagination when using cursors. I think that complexity is more at the application layer conceptually though; imagine the following:

  1. Page displays first 10 items starting at Add step to install babel globally to have access to babel-node #1; previous page unavailable, next page available
  2. Go to next page
  3. See first 10 items starting at Fix a typo #11; previous page available
  4. A new element #0 is prepended to the beginning
  5. Go to previous page
  6. Page displays 10 items starting at Add step to install babel globally to have access to babel-node #1; previous page available
  7. Go to previous page again
  8. Page displays only #0 (???)

I think there are meaningful practical difficulties with windowing on cursor-based APIs, which make it a bad enough fit that it might be better to not try to shoehorn it in.

@taion
Copy link
Contributor

taion commented Nov 10, 2015

To add: IMO one of the complexity points in Relay with e.g. automatically discovering which new nodes to fetch when using cursor-based pagination for infinite scrolling is just largely not relevant when using windowed pagination (via either page numbers or even limit/offset to an extent). That level of rich support just isn't as relevant in the windowed case.

@josephsavona
Copy link
Contributor Author

@taion Agreed, these are distinct use cases and ultimately Relay should support all of them. Allowing connection handling to be injectable would make it easier for products to choose between approaches, without having to build both models (page number & limit/offset are isomorphic) into the core and test them separately. Note that connections account for much of the complexity in Relay internals, so testing against one well-defined injection API is preferable to n arbitrary connection models.

@taion
Copy link
Contributor

taion commented Nov 10, 2015

That's not exactly what I'm saying - I feel like the current pagination API offers enough to (with at most minor tweaks) satisfactorily implement page-based pagination and limit/offset-based pagination in user space.

Limit/offset might have slightly different semantics, but it seems perfectly suitable to model e.g. page-based pagination as just another argument to the current connection style.

@shaimo
Copy link

shaimo commented Dec 16, 2015

I also would like to be able to jump to a specific page. @taion - you write above "I feel like the current pagination API offers enough to (with at most minor tweaks) satisfactorily implement page-based pagination". Can you please explain how to achieve this? Many thanks...

@taion
Copy link
Contributor

taion commented Dec 16, 2015

You just add a page arg or something to the field. Just don't bother with the cursor-related stuff.

@dminkovsky
Copy link
Contributor

to a GraphQLList-type field, right? And then just do whatever, right?

On Wed, Dec 16, 2015 at 10:56 AM, Jimmy Jia notifications@github.com
wrote:

You just add a page arg or something to the field. Just don't bother with
the cursor-related stuff.


Reply to this email directly or view it on GitHub
#540 (comment).

@taion
Copy link
Contributor

taion commented Dec 16, 2015

Pretty much. You can make it a connection if you want connection-style behavior on mutations... depends what you want, really. But windowed pagination is in some sense easy - it's just a custom filter arg.

@shaimo
Copy link

shaimo commented Dec 17, 2015

@taion - that's indeed easy - my concern was that using some arbitrary page parameter rather than the cursor I would be losing the benefits of the connection type. If there are no such benefits, then not even sure why bother with connection in the first place rather than just some standard field...?

@taion
Copy link
Contributor

taion commented Dec 17, 2015

Which specific benefits were you thinking about that would be relevant when doing windowed pagination?

@shaimo
Copy link

shaimo commented Dec 17, 2015

Not sure. I'm really new to Relay and might not have enough understanding of all the concepts, but I read somewhere that Connection was created to work well with large datasets. But if not using the Connection mechanisms and instead just using some page number parameter, is there any reason to stick with a Connection rather than a standard field?

@taion
Copy link
Contributor

taion commented Dec 17, 2015

You get nice stuff like just inserting new edges after mutations. Otherwise there are great conveniences for infinite scroll type views. But if you're just doing windowed pagination, I don't think it matters much.

@shaimo
Copy link

shaimo commented Dec 17, 2015

@taion Thanks for your help. I'll check if I can relax my requirements and maybe just use what Connection provides. Maybe indeed in large datasets it doesn't make much sense to allow the viewer to "jump" to a particular page anyway (especially if the dataset is not fixed, in which case next time you will get different results anyway)...

@BerndWessels
Copy link

@taion Can you please clarify what the nice stuff is exactly?
Even for windowed pagination I would like new edges to be inserted/removed "magically" after insert/delete mutations.
Is there other "nice stuff" that has to be considered?

@taion
Copy link
Contributor

taion commented Jan 14, 2016

That doesn't really make sense in the context of windowed pagination. Suppose you're on page n. If an insert happens, where the new node is not inserted on this page, what should you do? That's why I say it's not particularly well-defined.

@BerndWessels
Copy link

@taion Thats true, even though in some of my previous apps the visible window would be updated to reflect inserts and updated correctly. But that might be actually a bit out of scope here since it usually also requires a "real-time" connection or notifications from the back-end.

So what you are saying is: forget about connections and implement a simple windowed pagination as a simple query for a list - because connections do not provide any benefit in this case?

@taion
Copy link
Contributor

taion commented Jan 14, 2016

I think if you're doing inserts or deletes, using a connection will still be more like what you want – it's just that there will be additional edge cases to think about in the context of insertions and deletions. You're probably going to just end up re-fetching that entire page on insertions or deletions, which is probably what you want anyway.

@BerndWessels
Copy link

Thanks @taion. There is only one last thing I am concerned about - memory. What if a user pages through huge amounts of rows - maybe even in different connections - will the store get bigger and bigger - or is there some kind of garbage collection in the Relay Store as well?

@taion
Copy link
Contributor

taion commented Jan 14, 2016

You get the same thing no matter what pagination scheme you use.

@BerndWessels
Copy link

@taion I thought that Relay Connections might be able to remove unused edges from the Store. But maybe memory concerns is a totally different discussion.

@josephsavona
Copy link
Contributor Author

I'm going to close due to inactivity. However, in the new core we've developed a more generalized abstraction of pagination/connections that should allow developing windowed pagination in user-space. We'll document and revisit once the new core is available (#1369).

Thanks all - we really appreciate your being vocal about this use-case.

@mrdulin
Copy link

mrdulin commented Oct 17, 2019

same issue. I need to jump to a specific page when using relay-style cursor-based pagination. For example: first page(first: 10) => page 10(first: 10, after: ??), How can I calculate the cursor?

I have a pagination UI component like this:

image

So, hasNextPage and hasPreviousPage doesn't satisfy my requirements.

How can I achieve this? Thanks.

@jgcmarins
Copy link
Contributor

What you want is something called totalCount and skip.
Try to follow this implementation for totalCount

Based on the totalCount and the amount of items per page, it's possible to know how many pages you gonna have.

After that, to jump into a specific page, you probably gonna need a skip parameter.
Follow this implementation of skip.

@mrdulin
Copy link

mrdulin commented Oct 17, 2019

@jgcmarins Thanks. Should this Relay Cursor Connections Specification need to be updated? Or, there will be a new version Specification?

I didn't find skip parameter in this spec.

@jgcmarins
Copy link
Contributor

Specifications are right.
Skip is not related to Relay

@mzikherman
Copy link

In case anyone finds this useful, wrote a blogpost on using Relay/GraphQL for windowed pagination cc @mrdulin

https://artsy.github.io/blog/2020/01/21/graphql-relay-windowed-pagination/

@lambrosx77
Copy link

Hi,

I try to implement a pagination table with usePagination and if everything works perfectly for next and previous page, I can't find the good way for "go to page" or "last page".
If we try to force the cursor, we get "Relay: Unexpected after cursor , edges must be fetched from the end of the list" because of the need of cursor continuity.

I have checked all the project, documentation, links... and nothing help me for now.

Hooks simplify relay but it stays infortunately hard with uncomplete up to date documentation. I know it's time but if we want membership...

Anyway, thank you for all this amazing project.

@mercurio-developing
Copy link

I resolved this getting the total count of items with that I get the number of pages, and the page you click * number of items per page, and calls the query with variable first = page number * number of items per page and pass also last with the number of items per page. Example 100 items 20 per page 5 pages click page 3 it will 3 * 20 = 60 this value it will be first and last 20 you get the items from 40 to 60.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants