Relay: [meta] Support windowed pagination

Created on 2 Nov 2015  Â·  28Comments  Â·  Source: facebook/relay

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.
enhancement help wanted

Most helpful comment

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.

All 28 comments

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

@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?

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 #1; previous page unavailable, next page available
  2. Go to next page
  3. See first 10 items starting at #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 #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.

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.

@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.

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.

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...

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

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

On Wed, Dec 16, 2015 at 10:56 AM, Jimmy Jia [email protected]
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
https://github.com/facebook/relay/issues/540#issuecomment-165153633.

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.

@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...?

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

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?

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.

@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)...

@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?

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.

@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?

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.

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?

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

@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.

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.

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.

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.

@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.

Specifications are right.
Skip is not related to Relay

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/

Was this page helpful?
0 / 5 - 0 ratings

Related issues

brad-decker picture brad-decker  Â·  3Comments

derekdowling picture derekdowling  Â·  3Comments

jstejada picture jstejada  Â·  3Comments

leebyron picture leebyron  Â·  3Comments

sibelius picture sibelius  Â·  3Comments