Pagination in Headless WordPress with WPGraphQL, Apollo, & Next.js
Tags: nextjs, wpgraphql, apollo, wordpress
Published: 2023-02-07 16:26:19

In this article, I will discuss how WPGraphQL does cursor-based pagination in headless WordPress along with using Next.js and Apollo GraphQL for my client.

## Pagination 📃

Pagination on the web is defined as a process of breaking or separating the content of a website into different pages. The user can use links such as “next,” “previous” and page numbers to navigate between pages and display sets of data when there is too much data to display all of the records at one time.

### Cursor-based Pagination in WPGraphQL

Out of the box, WPGraphQL ships with what is called [cursor-based](https://www.wpgraphql.com/2020/03/26/forward-and-backward-pagination-with-wpgraphql) pagination. This method loads the initial set of records and provides a cursor which is the reference point for the next request to utilize it and make the request for the next batch of records.

Traditional WordPress uses what is called offset-based pagination. This method groups post within the page numbers, essentially putting parameters within the client requests with a specific limit or number of results and then offset, the number of records that need to be skipped.

I won’t go too deep into the pros and cons of each in this article. Please check out Jason Bahl’s [article](https://www.wpgraphql.com/2020/03/26/forward-and-backward-pagination-with-wpgraphql) here if you want to get a deeper dive into each method.

## WPGraphQL Queries with Cursor-based pagination 🐘

Let’s navigate to the WP Admin and GraphiQL IDE now to discuss how to query WPGraphQL with cursor-based pagination. Below is a screenshot of my initial query with the fields and arguments that come out of the box for pagination with WPGraphQL: 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h3sbwyxkp2u2hrs6xlmz.png)

In this initial query, I am asking for a list of my posts, with the arguments of grabbing the first 3 posts and then after that "null" or starting from the beginning of the list.    

What if instead of starting at the beginning of the list, I want to request the list of posts after the first 3?  This is where the cursor field and `pageInfo` type come in.  

The `pageInfo` type has 2 fields called `hasNextPage` and `endCursor`.  The `hasNextPage` field allows you to find out if you have more pages with your posts on that data set.  The `endCursor` field is a unique identifier string that represents the last post of the data set you are requesting.  It points to that last post on the request as shown here:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7rkcw986rwsa47ebiacs.png)


Essentially, I can now ask for every post before or after that unique identifier string that `endCursor` gives me instead of starting from the beginning. In this case, the post tied to that unique ID is "
Obi-Wan". When I grab the unique ID string and use it with the `after` argument instead of `null`, the query will start from that post and give me all the posts after it:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5r3ilej2xhedjdro8tfb.png)

This opens up a realm of other possibilities. You can just swap out the end cursor and fire off queries to get the next subset of results after the last one from that cursor. You can do it bi-directionally as well where you can get the last 3 before the end cursor, paginating both forward and backward.

There are performance gains in this method.  Because it uses the unique ID to locate the record and then counts forward or backward from that ID instead of loading every dataset in the case of offset pagination, it requires fewer resources in loading batches of data.

Let's re-write our query so that we can dynamically pass in the argument in the fields instead of hard coding the string, like so:

```
query getPosts($first: Int!, $after: String) {
    posts(first: $first, after: $after) {
      pageInfo {
        hasNextPage
        endCursor
      }
      edges {
        node {
          id
          databaseId
          title
          slug
        }
      }
    }
  }
```
We are accepting input arguments `first` which is an integer and `after` which is a string.  In our front-end app, we can pass in the first 3 then when the user hits the load more button, our app then grabs the end cursor and will pass in different query variables to get the next set of data results on whichever end cursor string was tied to that post.

This query is now ready to use on our first pagination example called Load More which will be used in our Next.js front-end using the Apollo client.

## Load-More in Next.js and Apollo

In my Next.js application, I have a file called `LoadMorePost.js` which is a component that lives in my component folder:

```
import { useQuery, gql } from "@apollo/client";
import Link from "next/link";

const GET_POSTS = gql`
  query getPosts($first: Int!, $after: String) {
    posts(first: $first, after: $after) {
      pageInfo {
        hasNextPage
        endCursor
      }
      edges {
        node {
          id
          databaseId
          title
          slug
        }
      }
    }
  }
`;

const BATCH_SIZE = 5;

export default function LoadMorePost() {
  const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
    variables: { first: BATCH_SIZE, after: null },
    notifyOnNetworkStatusChange: true,
  });

  if (error) {
    return <p>Sorry, an error happened. Reload Please</p>;
  }

  if (!data && loading) {
    return <p>Loading...</p>;
  }

  if (!data?.posts.edges.length) {
    return <p>no posts have been published</p>;
  }

  const posts = data.posts.edges.map((edge) => edge.node);
  const haveMorePosts = Boolean(data?.posts?.pageInfo?.hasNextPage);

  return (
    <>
      <ul style={{ padding: "0" }}>
        {posts.map((post) => {
          const { databaseId, title, slug } = post;
          return (
            <li
              key={databaseId}
              style={{
                border: "2px solid #ededed",
                borderRadius: "10px",
                padding: "2rem",
                listStyle: "none",
                marginBottom: "1rem",
              }}
            >
              <Link href={`/blog/${slug}`}>{title}</Link>
            </li>
          );
        })}
      </ul>
      {haveMorePosts ? (
        <form
          method="post"
          onSubmit={(event) => {
            event.preventDefault();
            fetchMore({ variables: { after: data.posts.pageInfo.endCursor } });
          }}
        >
          <button type="submit" disabled={loading}>
            {loading ? "Loading..." : "Load more"}
          </button>
        </form>
      ) : (
        <p>✅ All posts loaded.</p>
      )}
    </>
  );
}

```

Let’s break this file down into chunks. At the top of the file, I am importing the `useQuery` hook and `gql` provided by the Apollo client that I am using as well as `next/link` from Next.js. We will need these imports in this file.

The next thing you see is the query we created in GraphiQL back in our WordPress admin with the assistance of WPGraphQL which will allow us to fire off requests to WPGraphQL and use cursor-based pagination.



The following line shows the number of posts I want to grab in a constant in `BATCH_SIZE`. When the user hits the load more button, it will populate 5 posts in each load.

```
const GET_POSTS = gql`
  query getPosts($first: Int!, $after: String) {
    posts(first: $first, after: $after) {
      pageInfo {
        hasNextPage
        endCursor
      }
      edges {
        node {
          id
          databaseId
          title
          slug
        }
      }
    }
  }
`;

const BATCH_SIZE = 5;

```
After that, I have a default components function called `LoadMorePost`. In this function, I am making use of the `useQuery` hook in Apollo to pass in my query called `GET_POSTS` from the top of the file. Next, I have variables that I pass in, which was the batch size I defined to be 5 and after `null` or start from the beginning. This function gets fired off each time the user clicks the “load more” button.

Following that, I have some `if` conditionals that invoke execution of possible states if an “error,” “loading,” or if we have no posts and the request has finished then we have no more posts published. If those checks have all passed, it means we have posts to be displayed.

```
export default function LoadMorePost() {
  const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
    variables: { first: BATCH_SIZE, after: null },
    notifyOnNetworkStatusChange: true,
  });

  if (error) {
    return <p>Sorry, an error happened. Reload Please</p>;
  }

  if (!data && loading) {
    return <p>Loading...</p>;
  }

  if (!data?.posts.edges.length) {
    return <p>no posts have been published</p>;
  }
```

There are 2 variables that get set next. The first variable is `posts` which is taking the data that Apollo gives us back and drilling down into it with the posts and their nested data. The second variable is `haveMorePosts` which checks if we have more posts to load but if there are no more posts we will have to execute something else.

```
 const posts = data.posts.edges.map((edge) => edge.node);
  const haveMorePosts = Boolean(data?.posts?.pageInfo?.hasNextPage);
```
So now we can display our posts with a `return` statement with some data drilling within the levels of nesting that comes from the query.

Focusing now on the `return` statement, we have a `<ul>` tag. Within that tag, we are mapping over posts and returning a single post with a `databaseId`, `title`, and its `slug`. For each of those, we are displaying a list item with a `<li> `tag. This list item will have a title that has a link to the actual individual blog post’s page.

```
  <ul style={{ padding: "0" }}>
        {posts.map((post) => {
          const { databaseId, title, slug } = post;
          return (
            <li
              key={databaseId}
              style={{
                border: "2px solid #ededed",
                borderRadius: "10px",
                padding: "2rem",
                listStyle: "none",
                marginBottom: "1rem",
              }}
            >
              <Link href={`/blog/${slug}`}>{title}</Link>
            </li>
          );
        })}
      </ul>
```

Lastly, we have to add a “load more” button. This button when clicked will load the next batch of posts from the cursor’s point. In order to do this, we take our `haveMorePosts` boolean and if we do have more, we will display a form with a button inside of it. When that button is clicked, we have a `onSubmit` handler that calls the `fetchMore` function in Apollo and passes in the variable called `after` that grabs the current end cursor, which is the unique ID that represents the last post in the data set to grab the next 5 after that end cursor.

```
 {haveMorePosts ? (
        <form
          method="post"
          onSubmit={(event) => {
            event.preventDefault();
            fetchMore({ variables: { after: data.posts.pageInfo.endCursor } });
          }}
        >
          <button type="submit" disabled={loading}>
            {loading ? "Loading..." : "Load more"}
          </button>
        </form>
      ) : (
        <p>✅ All posts loaded.</p>
      )}
    </>
  );
}

```

This is done and I have placed this component in `pages/load-more.js` within my Next.js app to give it a route and a page.

Let’s see how this all looks in action:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cs8wrf8jhtbuk8z9lngm.png)

## Conclusion 🚀

Pagination is an important part and very common in modern websites and apps.  In this article, I hope you took away a better understanding of how to paginate in headless WordPress with the best-in-class frameworks and tools: WPGraphQL, Next.js, and the Apollo Client.

As always, stoked to hear your feedback and any questions you might have on headless WordPress! Hit us up in our discord!