August 9, 2016

Pagination and Infinite Scrolling in Apollo Client

Slava Kim

Slava Kim

We are building Apollo Client — a GraphQL client that helps you fetch data, organize your store, and keep your client state in sync with the server.

In modern web and mobile applications, users often consume endless streams of data. In a way, apps provide a “window” into the vast array of information that a service can provide: posts, photos, comments, messages and more. It’s now a widely-adopted interface to show part of big list on a screen while loading it part by part in the background, giving the user the feeling of navigating seamlessly. This is what developers call “Infinite Scrolling”.

Infinite Scrolling: new items are loaded as the scrolling reaches the bottom of the list

Built-in infinite scroll support was one of the most-requested features in Apollo Client, and we had many discussions in the GitHub issues about possible designs.

One important concern was ensuring that Apollo Client’s normal caching behavior worked in a reasonable way with this new feature. When Apollo Client fetches a query from the server, the result is normalized from a big, tree-like query result into individual objects with IDs and fields, enabling Apollo Client to update these objects from mutations (and soon subscriptions), and have the new results show up in all queries that reference that object.

After investigating lots of possible solutions, we came up with the solution I want to show you today: fetchMore.

The solution: fetchMore

The new fetchMore method on Apollo Client query observables provides a way for applications to query additional data and add it to the store, updating the original query result. This way, the application can take advantage of all the benefits of cache normalization while incrementally fetching new paginated data.

Here is an example of a query that fetches posts based on a cursor:

const query = client.watchQuery({
  query: gql`
    query Posts($cursor: String) {
      feed {
        nextCursor
        entries(cursor: $cursor) {
          id
          title
          content
          author {
            name
          }
        }
      }
    }
  `,
  // first fetch, no cursor provided
  variables: { cursor: null },
});

In Apollo Client, watchQuery returns an observable that can be fed into the UI or other parts of an application interested in this data. In this case, the result of the query includes a pagination cursor, which we would like to save and pass to the server with our next query to continue fetching from the same generated list.

When the application needs to load more data — for example, if the user scrolled through the whole list of available posts—you can simply call fetchMore with the new cursor to get more data. The updateQuery function specifies how the new results should be merged with the previous result:

// fetch more data, reusing the same query,
// but passing different variables
query.fetchMore({
  variables: { cursor: lastPaginationCursor },
  
  // concatenate old and new entries
  updateQuery: (previousResult, { fetchMoreResult }) => {
    const newEntries = fetchMoreResult.data.feed.entries;
    return { feed: {
      nextCursor: fetchMoreResult.data.feed.nextCursor,
      entries: [...previousResult.feed.entries, ...newEntries],
    }};
  },
});

Under the hood, Apollo Client figures out how the merged query results should be normalized into individual objects with updated fields based on their ids. The query observable will fire a new value and the UI listening to the observable will be updated accordingly.

Read more about using pagination features and fetchMore in the Apollo Client docs.

Configuration over convention

This API favors manual configuration over a more strictly specified pagination structure, like you would see in Relay. This is intentional. Our goal was to build an API that would be easy to use, yet flexible enough to work with any pagination schema you could implement in your GraphQL server: cursor-based, offset/limit, etc. It’s important to us that Apollo Client doesn’t impose any requirements on your GraphQL schema to enable pagination.

Better yet, this feature can be used to implement something that has properties similar to pagination (i.e. fetching partial data for the query over time) but doesn’t just append or prepend pages of data to a list. Since the API allows you to supply a function that accumulates results of the query in an arbitrary way, you can merge the new results any way you like, deduplicate items, sort them, or anything else you need to do.

We’re especially excited that this feature is the result of an extensive collaboration with the open-source community. Together we went through multiple iterations of proposals, implementations and review until we finally arrived to the solution we are shipping with Apollo Client today. Thanks to everyone who provided their input along the way, especially Robin Ricard!


If you’d like to use this feature yourself, try Apollo Client today!

Written by

Slava Kim

Slava Kim

Read more by Slava Kim