Pagination in Headless WordPress with WPGraphQL, Apollo, & Next.js

Francis Agulto Avatar

·

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 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 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:

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:

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:

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
        }
      }
    }
  }
Code language: PHP (php)

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>
      )}
    </>
  );
}

Code language: JavaScript (javascript)

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;

Code language: PHP (php)

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>;
  }
Code language: JavaScript (javascript)

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);
Code language: JavaScript (javascript)

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>
Code language: JavaScript (javascript)

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 fetchMorefunction 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>
      )}
    </>
  );
}

Code language: HTML, XML (xml)

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:

Stoked!! We have a “load more” option on our page for users to paginate posts!! ⚡

The Relay Spec and Apollo Client 🔗

The relay specification contains functionality to make manipulating one-to-many relationships easy, using a standardized way of expressing these one-to-many relationships. This standard connection model offers ways of slicing and paginating through the connection.

The Apollo client can implement relay-style pagination with the relay spec using merge and read functions, which means all the thorny details of connections and edges and pageInfo can be abstracted away, into a single, reusable helper function. WPGraphQL follows the relay spec as well.

In the abstract, Apollo has an initial list, and when you call fetchMore it adds the list together and stores those in memory and that becomes the list that you are working with. Apollo does this appending for you under the hood. Let’s take a look at our apollo.js file to see how this is done:

import { ApolloClient, InMemoryCache } from "@apollo/client";
import { relayStylePagination } from "@apollo/client/utilities";

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        posts: relayStylePagination(),
      },
    },
  },
});

export const client = new ApolloClient({
  uri: process.env.NEXT_PUBLIC_WORDPRESS_API_URL,
  cache,
});

Code language: JavaScript (javascript)

We initialize the cache variable to a new InMemoryCache instance being imported at the top of the file from Apollo. Then we can add and define typePolicies, which is a configuration object used to customize the cache’s behavior on a type-by-type basis.

In this case, what we are defining in our types here are the fields of the posts type. Once those are defined, we can now use the relay spec used by WPGraphQL to tell Apollo and its spec in our pagination method to go ahead and append the previous and next list together using cursor-based pagination by calling the relayStylePagination function provided.

At the bottom, we create a new Apollo client by providing it with our GraphQL endpoint and the cache we created in the last step.

Previous and Next Posts

The next functionality I am going to add to my site is being able to click links on a single blog post page’s footer. These links will allow the user to go to the previous or next post from the starting point of the individual blog post page they are currently on.

The first thing I will have to do is modify the WPGraphQL schema so that we can query for the previous and next posts. Currently, you can’t do that out of the box.

In order to do this, let’s go back into our WP Admin. Back in our WP Admin, I took a PHP file that I got from Kellen Mace and compressed it into a zip file. I took this zip file and downloaded it into my plugins directory. I named it “pagination-fields”:

With this plugin extending and modifying our WPGraphQL schema, we can now query the post node for previous and next post links.

Just a note, WPGraphQL does not provide this because the previous and next posts can mean many different things to different types of projects and data. An example could be based on publish date or category archive or author archive etc. There are many variables in the context of what previous and next links could mean.

Our query for the previous and next posts will look like this:

 query getPost($slug: ID!) {
    post(id: $slug, idType: SLUG) {
      databaseId
      title
      content
      slug
      previousPost {
        title
        slug
      }
      nextPost {
        title
        slug
      }
    }
  }
Code language: PHP (php)

In this query, I am querying for a single post via its slug as the variable. At the bottom of the query is where I am able to also query for the previous and next posts with the single post query as the starting point. This is the result when you press play in GraphiQL:

Stoked! The query works. We can now use it in our Next.js app.

Finalizing previous and next posts in Next.js

Back in my Next.js app, navigating to the pages/blog/[slug].js file, our code looks like this:

import Link from "next/link";
import Head from "next/head";
import parse from "html-react-parser";
import { gql } from "@apollo/client";

import { client } from "../../lib/apollo";

export default function Post({ post }) {
  const { title, content, previousPost, nextPost } = post;

  return (
    <>
      <Head>
        <title>{parse(title)} | Pagination Station</title>
      </Head>
      <main>
        <article>
          <header>
            <h1>{title}</h1>
          </header>
          <div>{parse(content)}</div>
          <footer style={{ display: "flex" }}>
            {previousPost ? (
              <div style={{ border: "2px solid #ddd", padding: "1rem" }}>
                <Link href={`/blog/${previousPost.slug}`}>
                  👈 {previousPost.title}
                </Link>
              </div>
            ) : null}
            {nextPost ? (
              <div
                style={{
                  border: "2px solid #ddd",
                  padding: "1rem",
                  marginLeft: "1rem",
                }}
              >
                <Link href={`/blog/${nextPost.slug}`}>{nextPost.title} 👉</Link>
              </div>
            ) : null}
          </footer>
        </article>
      </main>
    </>
  );
}

export async function getStaticPaths() {
  const slugs = await getPostSlugs();
  const paths = slugs.map((slug) => {
    return { params: { slug } };
  });

  return {
    paths,
    fallback: false,
  };
}

async function getPostSlugs() {
  const { data } = await client.query({
    query: gql`
      query getPosts {
        posts(first: 100) {
          nodes {
            slug
          }
        }
      }
    `,
  });

  return data.posts.nodes.map((node) => node.slug);
}

const GET_POST = gql`
  query getPost($slug: ID!) {
    post(id: $slug, idType: SLUG) {
      databaseId
      title
      content
      slug
      previousPost {
        title
        slug
      }
      nextPost {
        title
        slug
      }
    }
  }
`;

export async function getStaticProps(context) {
  const { data } = await client.query({
    query: GET_POST,
    variables: {
      slug: context.params.slug,
    },
  });

  return {
    props: {
      post: data.post,
    },
  };
}

Code language: JavaScript (javascript)

This code block is using a few of Next.js’s conventions and functions. If you need a foundational understanding of the Next.js framework, please reference Jeff Everhart’s article. The file itself is a dynamic route file that utilizes the bracket syntax for whatever the parameter is in the file. In this case, it is the slug.

Breaking the file down, at the bottom of the file, I have a couple of asynchronous functions that Next.js provides called getStaticPaths and getStaticProps.

In my getStaticPaths function, I am nesting a second function called getPostSlugs which gets all the slugs for all posts from WordPress and maps over that array of paths to return a slug. This will tell Next.js what pages to use to build the dynamic routes.

export async function getStaticPaths() {
  const slugs = await getPostSlugs();
  const paths = slugs.map((slug) => {
    return { params: { slug } };
  });

  return {
    paths,
    fallback: false,
  };
}

async function getPostSlugs() {
  const { data } = await client.query({
    query: gql`
      query getPosts {
        posts(first: 100) {
          nodes {
            slug
          }
        }
      }
    `,
  });

  return data.posts.nodes.map((node) => node.slug);
}
Code language: JavaScript (javascript)

In the getStaticProps function, we are telling it what props to pass in from WordPress and query WPGraphQL with GET_POST for the specific post data we want back about that post including the next and previous post. When that function is run, we return the post results.

export async function getStaticProps(context) {
  const { data } = await client.query({
    query: GET_POST,
    variables: {
      slug: context.params.slug,
    },
  });

  return {
    props: {
      post: data.post,
    },
  };
}
Code language: JavaScript (javascript)

Turning our attention now to the top of the file, we have a default function called Post which will be the component to grab the destructured data in our const we are querying from WPGraphQL into our JSX. I have the data that I want for a single post page within a <div> for the content and a <h1> for the title.

export default function Post({ post }) {
  const { title, content, previousPost, nextPost } = post;

  return (
    <>
      <Head>
        <title>{parse(title)} | Pagination Stoke</title>
      </Head>
      <main>
        <article>
          <header>
            <h1>{title}</h1>
          </header>
          <div>{parse(content)}</div>
Code language: HTML, XML (xml)

Let’s now focus our attention on the footer section where we will pull our previous and next post data.

Starting with previousPost, if the post does exist, we display that previous post title. Using next/link, the user has access to a clickable link to route them to that previous post page. Otherwise, if we do not have a previous post, it will render null and nothing will appear.

After that, we have a similar JSX called nextPost which does the exact same thing as previousPost except it will show and render the next post.

 <footer style={{ display: "flex" }}>
            {previousPost ? (
              <div style={{ border: "2px solid #ddd", padding: "1rem" }}>
                <Link href={`/blog/${previousPost.slug}`}>
                  👈 {previousPost.title}
                </Link>
              </div>
            ) : null}
            {nextPost ? (
              <div
                style={{
                  border: "2px solid #ddd",
                  padding: "1rem",
                  marginLeft: "1rem",
                }}
              >
                <Link href={`/blog/${nextPost.slug}`}>{nextPost.title} 👉</Link>
              </div>
            ) : null}
          </footer>
Code language: HTML, XML (xml)

Now that we have that all coded up, let’s see this working in the browser:

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!