---
title: Pagination
---

import { MultiCodeBlock } from 'gatsby-theme-apollo-docs';

Often, you will have some views in your application where you need to display a list that contains too much data to be either fetched or displayed at once. Pagination is the most common solution to this problem, and Apollo Client has built-in functionality that makes it quite easy to do.

There are basically two ways of fetching paginated data: numbered pages, and cursors. There are also two ways for displaying paginated data: discrete pages, and infinite scrolling. For a more in-depth explanation of the difference and when you might want to use one vs. the other, we recommend that you read our blog post on the subject: [Understanding Pagination](https://medium.com/apollo-stack/understanding-pagination-rest-graphql-and-relay-b10f835549e7).

In this article, we'll cover the technical details of using Apollo to implement both approaches.

## Using `fetchMore`

In Apollo, the easiest way to do pagination is with a function called [`fetchMore`](../caching/cache-interaction/#incremental-loading-fetchmore), which is included in the result object returned by the `useQuery` Hook. This basically allows you to do a new GraphQL query and merge the result into the original result.

You can specify what query and variables to use for the new query, and how to merge the new query result with the existing data on the client. How exactly you do that will determine what kind of pagination you are implementing.

## Offset-based

Offset-based pagination — also called numbered pages — is a very common pattern, found on many websites, because it is usually the easiest to implement on the backend. In SQL for example, numbered pages can easily be generated by using [OFFSET and LIMIT](https://www.postgresql.org/docs/8.2/static/queries-limit.html).

<MultiCodeBlock>
<div data-language="hooks-js">

```jsx
const FEED_QUERY = gql`
  query Feed($type: FeedType!, $offset: Int, $limit: Int) {
    currentUser {
      login
    }
    feed(type: $type, offset: $offset, limit: $limit) {
      id
      # ...
    }
  }
`;

const FeedData({ match }) {
  const { data, fetchMore } = useQuery(
    FEED_QUERY,
    {
      variables: {
        type: match.params.type.toUpperCase() || "TOP",
        offset: 0,
        limit: 10
      },
      fetchPolicy: "cache-and-network"
    }
  );

  return (
    <Feed
      entries={data.feed || []}
      onLoadMore={() =>
        fetchMore({
          variables: {
            offset: data.feed.length
          },
          updateQuery: (prev, { fetchMoreResult }) => {
            if (!fetchMoreResult) return prev;
            return Object.assign({}, prev, {
              feed: [...prev.feed, ...fetchMoreResult.feed]
            });
          }
        })
      }
    />
  );
}
```

</div>
<div data-language="js">

```jsx
const FEED_QUERY = gql`
  query Feed($type: FeedType!, $offset: Int, $limit: Int) {
    currentUser {
      login
    }
    feed(type: $type, offset: $offset, limit: $limit) {
      id
      # ...
    }
  }
`;

const FeedData = ({ match }) => (
  <Query
    query={FEED_QUERY}
    variables={{
      type: match.params.type.toUpperCase() || "TOP",
      offset: 0,
      limit: 10
    }}
    fetchPolicy="cache-and-network"
  >
    {({ data, fetchMore }) => (
      <Feed
        entries={data.feed || []}
        onLoadMore={() =>
          fetchMore({
            variables: {
              offset: data.feed.length
            },
            updateQuery: (prev, { fetchMoreResult }) => {
              if (!fetchMoreResult) return prev;
              return Object.assign({}, prev, {
                feed: [...prev.feed, ...fetchMoreResult.feed]
              });
            }
          })
        }
      />
    )}
  </Query>
);
```

</div>
</MultiCodeBlock>

As you can see, `fetchMore` is accessible through the `useQuery` Hook result object. By default, `fetchMore` will use the original `query`, so we just pass in new variables. Once the new data is returned from the server, the `updateQuery` function is used to merge it with the existing data, which will cause a re-render of your UI component with an expanded list.

The above approach works great for limit/offset pagination. One downside of pagination with numbered pages or offsets is that an item can be skipped or returned twice when items are inserted into or removed from the list at the same time. That can be avoided with cursor-based pagination.

Note that in order for the UI component to receive an updated `loading` prop after `fetchMore` is called, you must set `notifyOnNetworkStatusChange` to `true` in your Query component's props.

## Cursor-based

In cursor-based pagination, a "cursor" is used to keep track of where in the data set the next items should be fetched from. Sometimes the cursor can be quite simple and just refer to the ID of the last object fetched, but in some cases — for example lists sorted according to some criteria — the cursor needs to encode the sorting criteria in addition to the ID of the last object fetched.

Implementing cursor-based pagination on the client isn't all that different from offset-based pagination, but instead of using an absolute offset, we keep a reference to the last object fetched and information about the sort order used.

In the example below, we use a `fetchMore` query to continuously load new comments, which will be prepended to the list. The cursor to be used in the `fetchMore` query is provided in the initial server response, and is updated whenever more data is fetched.

<MultiCodeBlock>
<div data-language="hooks-js">

```jsx
const MORE_COMMENTS_QUERY = gql`
  query MoreComments($cursor: String) {
    moreComments(cursor: $cursor) {
      cursor
      comments {
        author
        text
      }
    }
  }
`;

function CommentsWithData() {
  const { data: { comments, cursor }, loading, fetchMore } = useQuery(
    MORE_COMMENTS_QUERY
  );

  return (
    <Comments
      entries={comments || []}
      onLoadMore={() =>
        fetchMore({
          // note this is a different query than the one used in the
          // Query component
          query: MORE_COMMENTS_QUERY,
          variables: { cursor: cursor },
          updateQuery: (previousResult, { fetchMoreResult }) => {
            const previousEntry = previousResult.entry;
            const newComments = fetchMoreResult.moreComments.comments;
            const newCursor = fetchMoreResult.moreComments.cursor;

            return {
              // By returning `cursor` here, we update the `fetchMore` function
              // to the new cursor.
              cursor: newCursor,
              entry: {
                // Put the new comments in the front of the list
                comments: [...newComments, ...previousEntry.comments]
              },
              __typename: previousEntry.__typename
            };
          }
        })
      }
    />
  );
}
```

</div>
<div data-language="js">

```jsx
const MORE_COMMENTS_QUERY = gql`
  query MoreComments($cursor: String) {
    moreComments(cursor: $cursor) {
      cursor
      comments {
        author
        text
      }
    }
  }
`;

const CommentsWithData = () => (
  <Query query={MORE_COMMENTS_QUERY}>
    {({ data: { comments, cursor }, loading, fetchMore }) => (
      <Comments
        entries={comments || []}
        onLoadMore={() =>
          fetchMore({
            // note this is a different query than the one used in the
            // Query component
            query: MORE_COMMENTS_QUERY,
            variables: { cursor: cursor },
            updateQuery: (previousResult, { fetchMoreResult }) => {
              const previousEntry = previousResult.entry;
              const newComments = fetchMoreResult.moreComments.comments;
              const newCursor = fetchMoreResult.moreComments.cursor;

              return {
                // By returning `cursor` here, we update the `fetchMore` function
                // to the new cursor.
                cursor: newCursor,
                entry: {
                  // Put the new comments in the front of the list
                  comments: [...newComments, ...previousEntry.comments]
                },
                __typename: previousEntry.__typename
              };
            }
          })
        }
      />
    )}
  </Query>
);
```

</div>
</MultiCodeBlock>

## Relay-style cursor pagination

Relay, another popular GraphQL client, is opinionated about the input and output of paginated queries, so people sometimes build their server's pagination model around Relay's needs. If you have a server that is designed to work with the [Relay Cursor Connections](https://facebook.github.io/relay/graphql/connections.htm) spec, you can also call that server from Apollo Client with no problems.

Using Relay-style cursors is very similar to basic cursor-based pagination.  The main difference is in the format of the query response which affects where you get the cursor.

Relay provides a `pageInfo` object on the returned cursor connection which contains the cursor of the first and last items returned as the properties `startCursor` and `endCursor` respectively.  This object also contains a boolean property `hasNextPage` which can be used to determine if there are more results available.

The following example specifies a request of 10 items at a time and that results should start after the provided `cursor`.  If `null` is passed for the cursor relay will ignore it and provide results starting from the beginning of the data set which allows the use of the same query for both initial and subsequent requests.

<MultiCodeBlock>
<div data-language="hooks-js">

```jsx
const COMMENTS_QUERY = gql`
  query Comments($cursor: String) {
    Comments(first: 10, after: $cursor) {
      edges {
        node {
          author
          text
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;

function CommentsWithData() {
  const { data: { Comments: comments }, loading, fetchMore } = useQuery(
    COMMENTS_QUERY
  );

  return (
    <Comments
      entries={comments || []}
      onLoadMore={() =>
        fetchMore({
          variables: {
            cursor: comments.pageInfo.endCursor
          },
          updateQuery: (previousResult, { fetchMoreResult }) => {
            const newEdges = fetchMoreResult.comments.edges;
            const pageInfo = fetchMoreResult.comments.pageInfo;

            return newEdges.length
              ? {
                  // Put the new comments at the end of the list and update `pageInfo`
                  // so we have the new `endCursor` and `hasNextPage` values
                  comments: {
                    __typename: previousResult.comments.__typename,
                    edges: [...previousResult.comments.edges, ...newEdges],
                    pageInfo
                  }
                }
              : previousResult;
          }
        })
      }
    />
  );
}
```

</div>
<div data-language="js">

```jsx
const COMMENTS_QUERY = gql`
  query Comments($cursor: String) {
    Comments(first: 10, after: $cursor) {
      edges {
        node {
          author
          text
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;

const CommentsWithData = () => (
  <Query query={COMMENTS_QUERY}>
    {({ data: { Comments: comments }, loading, fetchMore }) => (
      <Comments
        entries={comments || []}
        onLoadMore={() =>
          fetchMore({
            variables: {
              cursor: comments.pageInfo.endCursor
            },
            updateQuery: (previousResult, { fetchMoreResult }) => {
              const newEdges = fetchMoreResult.comments.edges;
              const pageInfo = fetchMoreResult.comments.pageInfo;

              return newEdges.length
                ? {
                    // Put the new comments at the end of the list and update `pageInfo`
                    // so we have the new `endCursor` and `hasNextPage` values
                    comments: {
                      __typename: previousResult.comments.__typename,
                      edges: [...previousResult.comments.edges, ...newEdges],
                      pageInfo
                    }
                  }
                : previousResult;
            }
          })
        }
      />
    )}
  </Query>
);
```

</div>
</MultiCodeBlock>

## The `@connection` directive

When using paginated queries, results from accumulated queries can be hard to find in the store, as the parameters passed to the query are used to determine the default store key but are usually not known outside the piece of code that executes the query. This is problematic for imperative store updates, as there is no stable store key for updates to target. To direct Apollo Client to use a stable store key for paginated queries, you can use the optional `@connection` directive to specify a store key for parts of your queries. For example, if we wanted to have a stable store key for the feed query earlier, we could adjust our query to use the `@connection` directive:

```js
const FEED_QUERY = gql`
  query Feed($type: FeedType!, $offset: Int, $limit: Int) {
    currentUser {
      login
    }
    feed(type: $type, offset: $offset, limit: $limit) @connection(key: "feed", filter: ["type"]) {
      id
      # ...
    }
  }
`;
```

This would result in the accumulated feed in every query or `fetchMore` being placed in the store under the `feed` key, which we could later use for imperative store updates. In this example, we also use the `@connection` directive's optional `filter` argument, which allows us to include some arguments of the query in the store key. In this case, we want to include the `type` query argument in the store key, which results in multiple store values that accumulate pages from each type of feed.
