Building Chatty — Part 5: GraphQL Pagination

A WhatsApp clone with React Native and Apollo

Simon Tucker
React Native Training
17 min readApr 19, 2017

--

This is the fifth blog in a multipart series where we will be building Chatty, a WhatsApp clone, using React Native and Apollo. You can view the code for this part of the series here.

In this tutorial, we’ll take a brief look at how to paginate data with GraphQL. By progressively loading data instead of getting it all at once, we can greatly improve the performance of our app.

Here’s what we will accomplish in this tutorial:

  1. Overview different pagination strategies
  2. Identify the best pagination strategy to apply to Chatty
  3. Incorporate pagination in the Schema and Resolvers on our server
  4. Incorporate pagination in the queries and layout of our React Native client

Pagination Strategies

Let’s look at 3 common strategies for pagination and their primary advantages and disadvantages:

  1. Page Numbering
  2. Cursors
  3. Relay Cursor Connections

For a more in-depth reading on pagination strategies and when and how to use them, I highly suggest checking out Understanding pagination: REST, GraphQL, and Relay, by Sashko Stubailo.

Page Numbering

Think the o’s in Goooooogle search results. Page numbering in its naive form is super easy to implement in SQL with limit and offset:

Page numbering’s strength is in its simplicity. It’s a great strategy for dealing with static content or ordered content that won’t likely change during a user session.

Page numbering’s weakness is dealing with dynamic data. When items are added or removed from our dataset, we can end up skipping an element or showing the same element twice. For example, if we added a new element to our data set that belongs first in the paginated results, navigating to the next page will show the last element on the current page for a second time. Similarly, if the first element gets deleted, navigating to the next page would skip what would have been the first element on the new page.

However, if our paginated results are ordered by newest element and elements aren’t deletable, page numbering can be a great option for paginating our data, especially for infinite scrollers.

Cursors

Cursors look to solve the very problem page numbering presents. Cursors are a lot like a bookmark — we can stick it where we left off, and even if we shove more papers randomly into our book and tear a bunch out, we can still find where we last left off.

Let’s say we’re trying to show a paginated list of books ordered by title. With the cursor $after which is the title of the last book shown on the current page, we could get the next page of results in SQL as follows:

In GraphQL, we would need our query response to include cursors:

Cursors solve the challenges afflicting page numbering, but we can do even better! In this model, the only way for our client to know it’s on the last page is if we sent an extra request for more entries and received an empty response. Moreover, we can imagine more complicated scenarios where we would want to know the cursor for any given element in our results, not just the first or last one. We also should really strive to conform to a standardized response for any paginated query rather than making new ones up as we go. Enter, Relay Cursor Connections.

Relay Cursor Connections

Relay Cursor Connections specify a standardized GraphQL Query response for paginated data. In our previous booksByTitle example, it would look like this:

In a nutshell, the shape of the response — the “connection object” — holds two elements: edges and pageInfo.

Each edge contains a node which is the element itself — in our case the book — and a cursor, which represents the cursor for the node element. Ideally, a cursor should be a serializable opaque cursor, meaning we shouldn’t have to worry about its formatting for pagination to work. So to match the spec, our booksByTitle query should look more like this:

Where “Moby Dick” has been base-64 encoded. Our cursor based pagination should work just fine so long as we can reliably serialize, encode, and decode our cursor.

The other half of the connection object is pageInfo. pageInfo holds just two Booleans hasPreviousPage and hasNextPage that specify exactly what you’d expect — whether a previous page or next page is available.

With this connection object, we can execute a new query from any cursor with however many elements we want returned. We’ll save extra trips to the server when we’ve hit the beginning or end of a page. We also now have a standardized way of writing any paginated query. Sweet!

Really the biggest downside to Relay Cursor Connections is the amount of code and energy it takes to execute. We might also take a small hit in performance versus the other strategies as the resolver does a bit more work per element and the response is a little larger.

Pagination on the Server

Time to add pagination to Chatty!

First, let’s identify some places where pagination makes sense.

There is no doubt pagination is sorely needed for displaying messages in a group. If we showed every last message in a group thread off the bat, things would get ugly quickly. We can also use pagination to preview the most recent message for a group before a user taps into the group thread.

I can’t imagine there are going to be many occasions where a user would belong to hundreds of groups, so let’s hold off on paginating groups until we have good reason.

What about a user’s friends? Pagination here can get a bit dicier, but I’m going to make the executive call and say not today — this is a nice-to-have feature but it’s not need-to-have. Most people don’t have a ton of contacts. Even if the call gets a bit expensive, it likely wont be thatexpensive, certainly not until Chatty has hundreds of thousands of users. Maybe we’ll implement this in a future tutorial :)

First, it’s important to note that page numbering is a totally valid solution to our use case, and much easier to implement than Relay Cursor Connections. Our messages will always be ordered by most recent, and we’re not planning on making them deletable anytime soon. WhatsApp just added the ability to edit and delete posts, and they’ve been around for 8 years. Really, most cases for pagination can be covered with page numbering. And when we add subscriptions next tutorial, you can see that even when data is constantly getting added and deleted, we could still use page numbering without running into issues.

However, Relay Cursor Connections are the gold standard for GraphQL pagination, and even though page numbering would suit us just fine, we’re gonna go the harder route so we’ll be armed for tougher pagination cases down the line.

Let’s code it up!

Relay Cursor Connection Schema

When we request messages for a given group, we don’t use the messages query, we use group. We currently only request Messages within the context of a Group, and that makes sense because it's unlikely we'll just want messages on their own.

So if we query for Messages within a Group with a Relay Cursor Connection shape, it needs to look something like this:

Cool! Let’s first modify our Schema to fit this shape.

We need to declare three new types in our Schema for Relay Cursor Connections:

  1. MessageConnection -- the wrapper type that will hold the edges and pageInfo fields.
  2. MessageEdge -- the type used for edges and will hold the node and cursor fields.
  3. PageInfo -- the type use for pageInfo and hold the hasPreviousPage and hasNextPage fields.

We also need to change the Group's messages field to take in Relay Cursor Connection arguments and return a MessageConnection instead of an array of Messages:

Step 5.1: Update Schema with Relay Cursor Connection

Changed server/data/schema.js

Now instead of asking for all messages when we query for a group or groups, we will specify the first n MessageEdges after the cursor supplied (or the last n MessageEdges before the cursor supplied).

Relay Cursor Connection Resolvers

We need to update our resolvers in server/data/resolvers.js to meet the spec we've just specified.

Our first order of business should be to define the cursor we will use to track which messages to retrieve.

When we create new Messages in SQLite, the new Message's id is based on an monatomic incrementing integer -- a fancy way of saying that newer Messages will always a have higher id than older Messages. We can use this neat feature to base our cursor on the Message id! For example, if we requested the first 10 Messages after the Message with id = 25, we could run the following sequelize query:

However, remember that we should use a serializable opaque cursor, not an integer. We’ll simply convert the Message id to a base64 string to meet this spec.

After we receive the Messages from our sequelize query, we still need to convert our results to fit our MessageConnection type. We'll need to iterate through our returned Messages and create an edge for each one, with the Message as the node, and base64(Message.id) as the cursor.

Lastly, we need to determine hasNextPage/hasPreviousPage. This can be simply accomplished by querying whether there is another Message after/before the returned results. It's also a good idea to keep pageInfo querying as separate functions in case the client doesn't request it -- a nice little performance enhancement.

Okay, enough theory — here’s the code:

Step 5.2: Update Resolvers with Relay Cursor Connection

Changed server/data/resolvers.js

A quick test in GraphQL Playground shows everything is looking good:

base64(22) = “MjI=” so we should get results for the first 2 results older than message 22

Pagination in React Native

We’re going to update our React Native client to paginate messages with an infinite scroller when viewing a group thread.

FlatList has a function onEndReached that will trigger when the user has scrolled close to the end of the list (we can set how close is needed to trigger the function via onEndReachedThreshold). However, messaging apps like ours typically display newest messages at the bottom of the list, which means we load older data at the top. This is the reverse of how most lists operate, so we need to modify our FlatList to be flipped so onEndReached triggers when we're approaching the top of the list, not the bottom. We can use the inverted flag on FlatList which flips the display of the list with a nifty trick just using CSS.

Step 5.3: Use inverted FlatList for Messages

Changed client/src/screens/messages.screen.js

Now let’s update GROUP_QUERY in client/src/graphql/group.query.js to match our latest schema:

Step 5.4: Update Group Query with Relay Cursor Connections

Changed client/src/graphql/group.query.js

We now have the ability to pass first, after, last, and before variables into the group query called by our Messages component. Those variables will get passed to our messages field, where we will receive a MessageConnection with all the fields we need.

We need to specify how group should look on a first run, and how to load more entries using the same query. The graphql module of react-apollo exposes a fetchMore function on the data prop where we can define how to update our query and our data:

Step 5.5: Add fetchMore to groupQuery

Changed client/package.json

Changed client/src/screens/messages.screen.js

We’ve specified first: 10 in our initial run of the query. When our component executes this.props.loadMoreEntries, we update the after cursor with the cursor of the last edge from our previous results, fetch up to 10 more messages, and update our app’s state to push the edges to the end of our data set and set whether there is a next page.

Since we are returning edges now, we need to update our Messages component to look for group.messages.edges[x].node instead of group.messages[x].

We also need to modify the update function in our mutations to match our updated GROUP_QUERY variables.

We should also create and append an edge to our cached query data whenever we create a new Message. This means deriving the cursorfor the new Message we've created as well.

We finally need to update the Messages component to call this.props.loadMoreEntries when we call onEndReached:

Step 5.6: Apply loadMoreEntries to onEndReached

Changed client/src/screens/messages.screen.js

Boot it up for some pagination!

Let the pages flow!

We can also modify the Groups component to preview the most recent message for each group. Using the same methodology, we’ll first update USER_QUERY:

Step 5.7: Add most recent message to each Group in USER_QUERY

Changed client/src/graphql/create-group.mutation.js

Changed client/src/graphql/user.query.js

And then we update the layout of the Group list item component in Groups:

Step 5.8: Modify Group component to include latest message

Changed client/src/screens/groups.screen.js

🔥🔥🔥

Refreshing Data

We can apply some of the tricks we’ve just learned to also give users a way to manually refresh data. Currently, if a user sends a message to a group, this new message won’t show up as the latest message on the groups page.

We could solve this problem by modifying update within sendMessage to update the USER_QUERY query. But let’s hold off on implementing that fix and use this opportunity to test manual refreshing.

In addition to fetchMore, graphql also exposes a refetch function on the data prop. Executing this function will force the query to refetch data.

We can modify our FlatList to use a built-in RefreshControl component via onRefresh. When the user pulls down the list, FlatList will trigger onRefresh where we will refetch the user query.

We also need to pass a refreshing parameter to FlatList to let it know when to show or hide the RefreshControl. We can set simply set refreshing to check for the networkStatus of our query. networkStatus === 4 means the data is still loading.

Step 5.9: Manual Refresh Groups

Changed client/src/screens/groups.screen.js

Boot it!

It’s important to test for common use cases….

Now that we can see manual refreshing is working, let’s fix up update within sendMessage to update the USER_QUERY query so manual updating is only required for strange edge cases and not all cases!

Step 5.10: Modify createMessage mutation to update USER_QUERY

Changed client/src/screens/messages.screen.js

Great Success!

That’s it for Part 5. In the next part of this series, we will add GraphQL Subscriptions for real-time instant messaging!

As always, please share your thoughts, questions, struggles, and breakthroughs below!

You can view the code for this tutorial here

Continue to Building Chatty — Part 6 (GraphQL Subscriptions)

--

--