GraphQL cursor pagination in Rails.
So you implemented some graphQL types but it just turns out that returning bazillions of records with each query introduces some performance issues. "It would be nice to have some pagination" you think to yourself. In this post we gonna build graphQL cursor-based pagination functionality. The intent of this post is to show as simple and straightforward as possible how to implement graphQL pagination in graphql-ruby. We'll also cover some nasty edge cases, so they won't ambush you in the wild ;)
I assume you are using the following ruby and gem versions:
First, let's create post model:
And seed some sample data to play with:
It's time for graphQL post type:
Let's attach it to graphQL schema:
For now, query for this resource looks like this:
and graphQL is politely returning all hundred posts:
But of course it's not quite what we want. So, let's implement basic cursor pagination.
For type with cursor pagination - aka connection, as it has been named by Facebook - we need to define... connection. To do so we are going to add another field to query type:
As you can see it's pretty straightforward. The interesting part here is invocation of connection_type which will create connection type for Types::PostType. Actually, we are done! Now the simplest query for this connection type goes like this:
which returns records in following form:
Query and its result looks more complicated, that's for sure, but where is the actual pagination? For this, graphql-ruby exposes another field in all connection types called pageInfo. There is also an additional cursor field under node which will contain a unique id for given node (it's not identical to post record id!). The final query takes following form:
As you probably noticed postsConnection above takes additional argument: first which is pretty self explanatory: "query for the first 3 edges (records)". Aforementioned pageInfo field provides us with, as name suggests, pagination-related info. It's fields are self explanatory I believe. The final result:
to get to the next page we'll provide postsConnection with additional argument:
...which translates to: "query for the first 3 records after node with cursor(node id): NA=="
Gotcha 1: hasPreviousPage and hasNextPage
You might presume that hasPreviousPage and hasNextPage will provide you with info suggested by its name. GraphQL has a little nasty suprise for you there: hasNextPage has valid info only when you paginate forward, for example: postsConnection(first: 3). Analogous hasPreviousPage when you go backward: postsConnection(last: 3). It will become more clear when you'll look at the example below:
Let's query for first 5 cursor ids:
First 5 cursors:
Now let's query for 2 records after second one Mg==:
We get pageInfo:
Despite 2 records still being on the previous page, we still get "hasPreviousPage": false. That's right: not null or undefined but obviously incorrect false. And it's not a bug. It's this way by design.
Explanation for this is that at Facebook, where GraphQL has its origins, infinite scroll is a way to go for pagination(news feed for example). And for this purpose, information about previous page when you scroll down is just not needed. It would only introduce unnecessary performance overhead. One simple way to mitigate this in the current state of affairs(not very elegant though...) is to make additional query, just to check if there is a previous page. So for the query above it would look like this:
..which provides us with correct hasPreviousPage info this time:
Gotcha 2: pageCount
So, you would probably like to know how many pages there is actually. To calculate this, you could do the following: total_records_count / records_per_page_count. So all you really need is info about total records count. It would be nice to query for it like this:
Unfortunately, running this query will result in:
In fact, there is no way to get this info from default connection. The reason is the same as in the case of previous and next page info: total records count is not really needed for infinite scroll pagination and would only introduce performance overhead. The good news is that it can be quite easily implemented though :)
First, we need to define manually our custom connection:
So, to define our custom connection we define a class which inherits from a base graphql connection class: GraphQL::Types::Relay::BaseConnection. We also have to define by hand an edge class PostsEdgeType and then specify that we are going to use it in our connection: edge_type(PostsEdgeType). Finally way we can add additional fields, like total_count in our example, just the way you would do this in other type classes, like Types::PostType.
Next, we need to specify that we are going to use Types::PostsConnection as posts_connection type class:
Now, our query with total count works as expected:
The drawback of this approach, is of course, that with totalCount we are introducing N+1 to our connection. Also, watch out for class name used for our custom connection. It has to end with Connection (as in our case Types::PostsConnection) to be treated by graphql-ruby as connection. If you need a different naming convention you can state explicitly that a connection type will be used in field definition:
GraphQL, along with its implementations (like graphql-ruby in our case) is still a very young technology and under rapid development. Thus, there is lack of conventions that would be widely accepted by community, as we are used to in the Rails world. This also applies to graphQL pagination and we should expect changes to its specification and implementation in the future. But until then, this post should prove to be useful on your graphQL path ;)
Full application code can be found here.