Open gallagher-stem opened 8 years ago
I know this seems counter-intuitive to you as it did to me as well, but that is actually how it is supposed to work...
hasPreviousPage is only meaningful when last is included, as it is always false otherwise. hasNextPage is only meaningful when first is included, as it is always false otherwise. When both first and last are included, both of the fields are set according to the above algorithms, but their meaning as it relates to pagination becomes unclear. This is among the reasons that pagination with both first and last is discouraged.
https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo.Fields
Sorry I couldn't use markdown in the post I emailed.
I also posted about this, was embarrassing hehe.
https://github.com/graphql/graphql-relay-js/issues/55
Thanks for the response, Matt.
So its the actual Relay spec that has a bug. :(
In any case, it makes no sense, and ruins bi-directional paging.
Since the maintainers of this repo are the same guys maintaining the spec, I'm gonna leave this open, as it really needs to be addressed.
Yeah, this is good feedback.
We went with this approach because it's easy to envision a backend system (and we had some at Facebook) where it was be prohibitively expensive to compute hasPreviousPage
when paginating forward (or vice versa). The cursors that we were using for pagination made it very efficient to skip straight to the item in question, but trying to traverse backwards to see if there was anything before it would have been costly. Hence, we didn't assign meaning to hasPreviousPage
, because we had no way to provide an accurate value there.
Your feedback is spot on, though; this definitely messes with bidirectional pagination. One way forward would be to modify the Relay specification and allow, though not require, hasPreviousPage
to have the expected meaning. Connections where hasPreviousPage
is easy to compute can then include it, where cases where it is expensive can continue returning false
. Clients can then enable the bidirectional pagination behavior you're looking to add only if they ever see hasPreviousPage
be true when paginating forward.
Thoughts?
Thanks for the reply, Dan. Loving the Relay.
tl;dr: connectionFromArray should assume that its got an entire dataset, so it can be honest about hasNextPage/hasPreviousPage. connectionFromArraySlice should assume that its got only part of a larger data set, so it cant be honest about hasPrevious page when its going forward (or vice-versa).
At the end of the day, to make good on the Relay goal of making data fetching/caching as easy and opaque as possible (which is an awesome goal), paging just "needs to work". And that means supporting the commonly understood ideas of what paging in apps looks like, which is pretty much either infinite scroll (forward-only) or next/prev (bi-directional) with/without page numbers.
We can check off forward-only because that works great! ;)
I was messing with arrayconnection.js and it was pretty simple to stop taking last/first into account when calculating and comparing the array bounds and the current cursor location. Relay on the client thwarted me, however, because it still overrides the values for hasNextPage/hasPreviousPage on the client based on whether first/last is being used.
I guess I don't understand why connectionFromArray can't give back honest values from hasNextPage/hasPreviousPage. Its assumed that we are passing in the whole list of data as the array. Why not be honest with the hasNextPage/hasPreviousPage?
connectionFromArraySlice can and should be used when we are using forward-only paging on large data-sets. The comments in the code come out and say as much. It kinda looks like we just found an easy way to have connectionFromArray delegate its work to connectionFromArraySlice. And connectionFromArraySlice correctly assumes it cant honestly answer if there is a previous page when going forward on what it has been told is just a slice of a larger array.
Maybe some renaming would make things clearer. connectionFromArray becomes bidirectionalPagingConnection (or entireDataSetConnection or something) and connectionFromArraySlice becomes forwardOnlyPagingConnection (or partialDataSetConnection or something).
And then change connectionArgs validation. In fact, Im not convinced we need both 'first' and 'last' if we can just make some assumptions. Instead of first/last we just have 'count', because thats what both those are: just a count of records to return. Then if we have count without before or after, we assume its going forward from the start (which I think is the 99%+ use case). If we want to start out going backward from the end, you pass a 'before: end' (or before: '' or something). Other than that, before and after cursors work the same, and args validation just changes to not allow both before and after together.
And then change the Relay spec to reflect these changes. ;)
(And then figure out page numbers, which once we decide that connectionFromArray is meant for entire datasets, is an easy prop to add to pageInfo and connectionArgs).
To maybe help out anyone currently wrangling with this same issue, here is a link to a gist that shows how I am hacking together next/prev paging in Relay. It was heavily informed by this fine gentleman's efforts with the same problem.
Anyway, thanks for the listen. I'm having so much fun getting into Relay and I love what it does and what its going to do. Rock on, rockstars!
I am trying to get hasNextPage
with connectionFromPromisedArray
but it does not work (returns false). first: 5
i am passing, there are more rows in database. It seems like it will work only with connectionFromArraySlice
because graphql-relay will get all data set and slice it. I read spec but still do not understand how it expected to work
Ugh, this issue is so deeply annoying when having a hard requirement to do windowed pagination due to limited space. Even with a server returning hasPreviousPage
as true
(because it can actually calculate that), hasPreviousPage
gets passed on to components as false
. Are there any decent examples of anyone doing a workaround that supports windowed pagination? I could care less about jumping to pages, but accurate previous & next links is vital.
The pageType and hasNextPage/hasPrevPage is declared in connection.js, but the logic appears to be in arrayconnection.js https://github.com/graphql/graphql-relay-js/blob/997e06993ed04bfc38ef4809a645d12c27c321b8/src/connection/arrayconnection.js#L66-L126
This code here looks suspect(startOffset
, endOffset
, lowerBound
, and upperBound
):
return {
edges,
pageInfo: {
startCursor: firstEdge ? firstEdge.cursor : null,
endCursor: lastEdge ? lastEdge.cursor : null,
hasPreviousPage:
typeof last === 'number' ? startOffset > lowerBound : false,
hasNextPage:
typeof first === 'number' ? endOffset < upperBound : false,
},
};
Specifically , the startOffset
and endOffset
are not set unless you use "first" or "last":
The source code below is found in the same file, slightly above.
if (typeof first === 'number') {
endOffset = Math.min(
endOffset,
startOffset + first
);
}
if (typeof last === 'number') {
startOffset = Math.max(
startOffset,
endOffset - last
);
}
The upperBound
and lowerBound
are set like so:
var lowerBound = after ? (afterOffset + 1) : 0;
var upperBound = before ? beforeOffset : arrayLength;
Looks like you need to set after
, before
, first
, and last
and you will get dual pagination to work. I haven't tested but looking at the source code I can't see why not!
This is really annoying - I had to implement my own methods, even though I can generate these values properly in the GraphQL response:
const idxPrefix = 'idx---';
const fromBase64 = (encoded: string): string =>
Buffer.from(encoded, 'base64').toString('utf8');
const indexFromCursor = (cursor: string): number =>
parseInt(fromBase64(cursor).replace(idxPrefix, ''), 10);
const hasPreviousPage = (startCursor: string): boolean =>
indexFromCursor(startCursor) > 0;
const hasNextPage = (endCursor: string): boolean =>
(indexFromCursor(endCursor) + 1) % limit === 0;
I have a feeling that this will be a continued problem, or people will choose to not adopt Connections / Edges, and instead just paginate via hints from parameters in a Route.
The other workaround is to put state of graphql query in the url
Page 1
/list
Page 2
/list?after={endCursor1}
Page 3
/list?after={endCursor2}
Page 4
/list?after={endCursor3}
Going forward works well as it is, to go backward simply go back in history, i.e. window.history.go(-1)
If ?after= is not present, Previous button can be hidden
Any status on resolving this?
@ivosabev: I'm not aware of anybody actively working on this, but given Dan's comment (quoting below) I think it would be reasonable to accept a PR that modified the spec:
Your feedback is spot on, though; this definitely messes with bidirectional pagination. One way forward would be to modify the Relay specification and allow, though not require, hasPreviousPage to have the expected meaning. Connections where hasPreviousPage is easy to compute can then include it, where cases where it is expensive can continue returning false. Clients can then enable the bidirectional pagination behavior you're looking to add only if they ever see hasPreviousPage be true when paginating forward.
Any update or work around on this issue? @gallagher-stem comment seems like a good solution,
I have PRed a change to the spec to permit setting these values properly so that at least other implementations can do so without breaking technical compliance: https://github.com/facebook/relay/pull/2079
Are PRs for this still welcome or should this issue be closed as won't fix?
Any plans to change this still?
According to spec:
hasPreviousPage is used to indicate whether more edges exist prior to the set defined by the clients arguments. If the client is paginating with last/before, then the server must return true if prior edges exist, otherwise false. If the client is paginating with first/after, then the client may return true if edges prior to after exist, if it can do so efficiently, otherwise may return false. More formally: Some code
That should mean hasPreviousPage can be true, when you know there are edges before the current dataset.
As of right now, I'm using something similar to @staylor
@staylor I know this was a while back now but your hasNextPage
would only work if the total number of items wasn't exactly divisible by the limit?
const idxPrefix = 'idx---';
const fromBase64 = (encoded) =>
atob(encoded);
const indexFromCursor = (cursor) =>
parseInt(fromBase64(cursor).replace(idxPrefix, ''), 10);
const hasPreviousPage = (startCursor) =>
indexFromCursor(startCursor) > 0;
const hasNextPage = (endCursor, arrayLength) =>
(indexFromCursor(endCursor) + 1) % limit === 0;
// Assuming 12 items in the array and pages (limit) of 3
console.log(hasNextPage(btoa('idx---2'))); // true
console.log(hasNextPage(btoa('idx---5'))); // true
console.log(hasNextPage(btoa('idx---8'))); // true
console.log(hasNextPage(btoa('idx---11'))); // true, should be false
Aside, to be fair I don't expect you thought anybody would copy paste it like I did 😆
I'm instead computing it based on the array length:
const idxPrefix = 'idx---';
const fromBase64 = (encoded) =>
atob(encoded);
const indexFromCursor = (cursor) =>
parseInt(fromBase64(cursor).replace(idxPrefix, ''), 10);
const hasPreviousPage = (startCursor) =>
indexFromCursor(startCursor) > 0;
const hasNextPage = (endCursor, arrayLength) =>
(indexFromCursor(endCursor) + 1) < arrayLength;
// Assuming 12 items in the array and pages (limit) of 3
console.log(hasNextPage(btoa('idx---2'), 12)); // true
console.log(hasNextPage(btoa('idx---5'), 12)); // true
console.log(hasNextPage(btoa('idx---8'), 12)); // true
console.log(hasNextPage(btoa('idx---11', 12))); // false
Any updates? Really hope we can fix this...
I know this seems counter-intuitive to you as it did to me as well, but that is actually how it is supposed to work...
hasPreviousPage is only meaningful when last is included, as it is always false otherwise. hasNextPage is only meaningful when first is included, as it is always false otherwise. When both first and last are included, both of the fields are set according to the above algorithms, but their meaning as it relates to pagination becomes unclear. This is among the reasons that pagination with both first and last is discouraged.
https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo.Fields
Sorry I couldn't use markdown in the post I emailed. I also posted about this, was embarrassing hehe. #55
That's not true. It always tells you if it has previous page, but not when using connectionFromArraySlice
Any updates? Really hope we can fix this...
I solved it with this (I think)
+ connection.pageInfo.hasPreviousPage = connection.edges.length > 0 && offset > 0 && offset < total;
https://github.com/graphql/graphql-relay-js/blob/7422cfe8ccd1c2cd3923c7e1d5f8b7533c0e3fdd/src/connection/arrayconnection.js#L120
If my connection args don't have the 'last' parameter, then hasPreviousPage will always be false. Same problem if you are paging backwards and don't have the 'first' parameter in your connection args: hasNextPage will always be false.
But I can't have a 'last' parameter if I am paging forward using 'first' and 'after'. And I can't have a 'first' parameter if I am paging backwards using 'last' and 'before'. Babel-relay-plugin will throw an error on transpile.
So if I am paging forwards, I will always be told I have no previous pages, even when I do. And if I am paging backwards I will always be told I have no next pages, even when I do.
This has gotta be a bug. It kinda ruins bi-directional paging.
Can't we just make paging easier and let us pass first and last and before and after all as connection args (some of them as null depending on which way you are paging) without babel-relay-plugin blowing up?