GraphQL pagination: Cursor and offset tutorials

Published on October 28, 2024

GraphQL pagination: Cursor and offset tutorials

GraphQL pagination helps applications that need to display large lists of content, whether it's static content like blog posts or dynamic data such as social media feeds. Adding pagination to your GraphQL queries offers both user experience and performance benefits.

This tutorial explains and demonstrates offset-based and cursor-based pagination in GraphQL, and offers examples for implementing each approach into a simple React app using real-world GraphQL APIs.

What is GraphQL pagination?

GraphQL pagination means breaking up the retrieval of data from a GraphQL API endpoint into smaller, manageable chunks. It's analogous to taking a page at a time from a book, rather than the whole book at once.

Fetching large amounts of data at once can be memory intensive and slow down the performance of your applications, requiring both additional backend processing power and more bandwidth than loading smaller amounts of data at a time. Pagination solves this, making applications more efficient and faster-loading.

Pagination is also useful because it ensures a nicer user experience — by not overwhelming the user with too much data at once.

The two main types of pagination in GraphQL — offset-based and cursor-based — are designed to handle different use cases. 

What is GraphQL pagination?

Offset-based pagination in GraphQL

Offset-based pagination in GraphQL is the most straightforward and widely used of the two pagination methods.

An example query that implements offset-based pagination to retrieve the first ten blog posts from a GraphQL API is shown below.

Not all GraphQL APIs support offset pagination, so it's worth checking the API documentation before attempting to use it.

A GraphQL API that supports offset-based pagination will accept two parameters:

  • offset — this defines the number of records to skip.

  • limit — this defines the maximum number of records to return.

Continuing with the above example, if you wanted to retrieve the next 10 blog posts (numbers 11–20), you can just change the offset to 10, meaning the first 10 records would be skipped.

Note that the offset pagination parameters could have different names (for example, in the Contentful GraphQL API, the offset parameter is called "skip"). 

Real-world GraphQL offset pagination example with the Contentful API

You can use the Contentful GraphQL API playground to test a working example of offset pagination.

For this you'll need a Contentful account and an API access token, as well as some data in your Contentful account to request via the GraphQL API. For this example, we created a blog post content model and then created three specific blog posts as pieces of content. All of this was done in our account's default project space — spaces are a way of organizing projects, and you'll need your space ID along with your access token to use the Contentful GraphQL API playground. You can find your space ID in the URL which looks something like this: https://app.contentful.com/spaces/<SPACE_ID>/views/entries.

Once you've created your content, you can query it in the GraphQL API playground by pasting this link in your browser and adding your space ID and access token.

https://graphql.contentful.com/content/v1/spaces/<SPACE_ID>/explore?access_token=<ACCESS_TOKEN>

Start by pasting the below query, which returns all the blog posts (without pagination) into the playground, and then execute it to get results returned to you.

You may have noticed that this query looks slightly different from the very first example query that was shared — that's because GraphQL APIs written by different development teams may not behave exactly the same way, so make sure you check the documentation or use introspective queries to find the syntax to use.

Once you've run the query, the response will be returned from the example API endpoint. This consists of three blog posts.

Screenshot of Contentful GraphQL explorer

An example query for requesting all blog posts — and the result it returns in the Contentful GraphQL explorer.

Next, try adding pagination to the query. Use the limit parameter to set the number of blog posts per page to 2, and the skip parameter to skip the first two blog posts (effectively skipping page 1 as there are only two posts per page).

The result is a single blog post, as there are a total of three blog posts and the first two are skipped.

Screenshot of Contentful GraphQL explorer, with a changed query and result

When to use offset-based GraphQL pagination

If your dataset is dynamic (for example, in social media feeds, the post order is not fixed and constantly changes depending on engagement metrics), then offset-based pagination shouldn't be used, as the order and quantity of entries changes between page loads.

Offset-based pagination is also not ideal for very large datasets consisting of tens of thousands of records. This is because it can become slow when you have to skip lots of data.

Offset-based pagination tends to be used for static data stored in a CMS. This includes data like blog posts, ecommerce product listings (provided they don’t update too often), or data for internal tools like sales reports or employee records. 

Cursor-based pagination in GraphQL

Cursor-based pagination in GraphQL is a much more efficient pagination method. It's designed to handle fast-changing data and ensures stable results even as new items are added or removed during the pagination process. This makes it ideal for use cases such as social media feeds, where the content is frequently updated.

Cursor-based pagination involves a unique identifier called a cursor that marks the position of the last item fetched. This is used as a reference point to get the next set of results.

A basic example of GraphQL cursor-based pagination is shown below, over two different queries. In this example, the first query returns the first five social media posts, and the second query returns the next five social media posts.

Each query also returns some metadata, including whether there is a next page of data available (so you know whether you've reached the end of the dataset) and an endCursor value, which is a unique identifier for the last returned result of the query, and which can be used to query the second page of data, and so on.

For example purposes, let's assume that the first query returned an endCursor of "cursor123". If you add this value to the after parameter, it will return the next page of results.

The first query in cursor pagination returns a cursor for the final item, which is then used for subsequent queries to keep track of what has already been fetched.

Diagram showing two different queries used in cursor-based pagination, and which items are returned for each

Forward vs. backward cursor pagination

Many GraphQL APIs that offer cursor-based pagination allow for both forward and backward pagination. This allows the user to scroll back in time, and then go forward again, without losing their place.

Backward pagination tends to work in a very similar way to forward pagination. In the example above, you would implement backward pagination by replacing first with last and after with before. In addition, you would replace hasNextPage and endCursor with hasPreviousPage and startCursor.

Relay-style cursor pagination

Relay is a JavaScript framework developed by Meta that is commonly used for efficient fetching of data from GraphQL APIs that support this standard. Many APIs follow this standard, including Facebook, Instagram, GitHub, and Shopify.

Relay-style pagination is more complex than the basic example above, and introduces two new concepts: nodes and edges, which come from graph theory. 

A node is an individual item in the collection that you're querying (for example, if you were querying a collection of social media posts, a node would be a single post). An edge contains the node itself and it may also contain additional relationship metadata if needed — for example, the cursor (the node’s position in the collection for pagination purposes) or whether the viewer (person making the query) has liked a particular social media post.

In addition to nodes and edges, Relay also relies on metadata about the pagination of the collection. This is stored in a field called pageInfo, and includes the fields hasNextPage and endCursor. Storing this pagination metadata separately to the nodes and edges is what makes relay-style pagination so fast.

For those of you who want to understand more about Relay pagination, Relay provides a handy tutorial.

Real-world GraphQL cursor pagination example 

The GitHub GraphQL API uses cursor-based pagination for querying its data, and offers a GraphQL API explorer for testing queries. This API follows the Relay standard. In this example, we will paginate a list of GitHub repositories.

In the explorer, sign in to GitHub so it's connected to your account, and enter the following query.

This will return your first five GitHub repositories, along with the pageInfo object.

The endCursor parameter is the cursor for the value of the last repository of this query (the fifth one). You'll need this value to run your second query, which will fetch the next five repositories. Replace "cursor123" with the value of your own endCursor.

Tutorial: How to incorporate GraphQL pagination into React

The below code examples demonstrate how to use offset-based and cursor-based GraphQL pagination in React. This code is available in full in our graphql-pagination GitHub repo. 

To follow along with both examples, you'll need a React app created using create-react-app.

Offset pagination React example

This example uses the Contentful GraphQL API to display blog posts across different pages. You can reuse your Contentful space ID and access token from earlier. 

These should be stored as environment variables in an .env.local file in the root of your project directory.

REACT_APP_CONTENTFUL_SPACE_ID=<YOUR_SPACE_ID>

REACT_APP_CONTENTFUL_ACCESS_TOKEN=<YOUR_ACCESS_TOKEN>

Next, create a new component called GraphQLOffset.js in the src/components directory.

Screenshot of how the directory structure should look so far in Visual Studio Code

In order to implement pagination of blog posts in React, React needs to be aware of:

  • The total number of posts

  • The number of posts per page

  • The current page number

  • The list of posts it currently has in its possession 

React uses useState() to manage the state of these different variables, so you need to declare them first.

The next step is to add a useEffect() hook to fetch the data. This includes the GraphQL query that we used earlier, and sets the skip and limit parameters to use the above variables, so that whichever page is being fetched, the correct blog posts will be returned.

Once the data has been fetched, the fetchPosts() function should update the state of the different pagination variables.

Once the paginated data for posts is being retrieved, you can create a React component to display the list of currently loaded posts.

You can implement visual pagination controls in React using the react-paginate library, which you can install by running: 

npm install react-paginate 

Using the ReactPaginate component means you don't have to write the pagination logic yourself. You can also use the styles.css in our example repository to format the ReactPaginate component.

Once you've installed react-paginate (and optionally added the example CSS), add the ReactPaginate component directly below the PostList component.

Finally, complete this example React application by adding the logic for re-rendering the page when the user clicks on a page button.

Now you have a nice-looking pagination on your page, powered by GraphQL!

Screen capture of a React website listing blog posts using offset-based pagination

Cursor pagination React example

This example uses the GitHub GraphQL API to display a list of all your GitHub repositories. 

You'll need a GitHub access token to use this API. To get a token, sign in to GitHub, navigate to Settings > Developer settings > Personal access tokens > Tokens (classic), and click the Generate new token button.

Back in your React project, add the following to your .env.local file, replacing <INSERT_GITHUB_TOKEN_HERE> with your actual token.

REACT_APP_GITHUB_TOKEN=<INSERT_GITHUB_TOKEN_HERE>

Next, create a new component called GraphQLCursor.js in the src/components directory.

Similar to offset pagination, React needs to manage the state of the pagination variables. These variables differ from those used in offset pagination — here, React keeps track of endCursor and hasNextPage instead of the current page or total number of items. 

Once you have created the state for storing pagination data, create the function for fetching repositories from the GraphQL API. The below takes an optional endCursor parameter. 

The fetchRepositories function will be called under two different circumstances: once when the component initially mounts (using the useEffect function), and again when the user clicks the "Load More Repositories" button, which calls the loadMoreRepositories function.

Next, create a React component to display the paginated list of repositories.

Finally, add a "Load More Repositories" button directly below your RepositoryList component, and have it call the loadMoreRepositories function.

Cursor-based pagination is actually better suited to a "Load More Repositories" button (or infinite scrolling) than specific numbered pages, as cursor pagination doesn't technically keep track of numbered pages.

Now you can run your project with npm start and you will see cursor-based pagination in action! You should see a list of five GitHub repos, and when you click the "Load More Repositories" button, you'll see five more added to the list.

Screen capture of a React website listing GitHub repositories using cursor-based pagination

Best practices for GraphQL pagination

Best practices for developers building GraphQL APIs

  • Stop consumers of the API from requesting too many results per page: Limit the response size to 10 or 20 results to prevent consumers of the API from straining resources.

  • Use time-based cursors: These provide a more predictable experience. For example, you might structure a query like posts(first: 10, after: "2023-09-10T12:00:00Z") to ensure consistency in pagination for fast-moving content.

  • Decide whether you need to offer cursor-based pagination: This depends on whether your data is dynamic, or is very large. If not, offset-pagination will suffice.

  • Offer both forward and backward pagination: Offering both gives consumers of the API more flexibility when it comes to cursor pagination.

Best practices for developers loading data from GraphQL APIs

  • Batch-related queries: To avoid the N+1 problem, where a query for a list of ten items results in 11 separate queries, batch these related queries together using a tool like DataLoader. An example of when this problem could happen is if you had a separate request for each set of author details relating to ten different blog posts.

  • Use cursor-based pagination for dynamic data: When working with frequently updated data, cursor-based pagination ensures that no results are missed as the position and quantity of data changes. This provides a better experience.

  • Use cursor-based pagination for very large datasets: This is the more efficient method for querying GraphQL datasets of many thousands of rows or more.

  • If your app uses infinite scrolling, use cursor-based pagination: This is more suited to the continuous loading of data.

Choosing the right GraphQL pagination method

Cursor-based pagination is best suited for dynamic or very large datasets, but offset-based pagination is easier to use, provided your data is static and not too large.

Contentful is a composable content system where you can store your content as different data types and use this to power the data behind your app. Contentful’s GraphQL API uses offset-based pagination for efficient and simple retrieval of all of your content including blog posts, images, videos, and products.

You've already seen how easy the Contentful GraphQL API is to use. If you'd like to try out Contentful, check out our React starter guide for help getting started.

Subscribe for updates

Build better digital experiences with Contentful updates direct to your inbox.

David Fateh

David Fateh

Software Engineer, Contentful

David Fateh is a software engineer with a penchant for web development. He helped build the Contentful App Framework and now works with developers that want to take advantage of it.

Related articles

Contentful is a powerful tool for game studios to efficiently create game content across many disparate teams. See how it's done with Unity in this tutorial.
Guides

Unity game development with Contentful

February 1, 2024

Optimizing images for your site? Learn how creators use the imgix integration for Contentful to seamlessly share, manage and collaborate on visual assets.
Guides

Simplify image workflow and optimize visual experiences with imgix and Contentful

June 2, 2022

Front end as a service (FEaaS) speeds up development and reduces infrastructure costs. Here's what FEaaS is, how it works, and a list of FEaaS platforms.
Guides

Understanding front end as a service (FEaaS)

October 18, 2024

Contentful Logo 2.5 Dark

Ready to start building?

Put everything you learned into action. Create and publish your content with Contentful — no credit card required.

Get started