How to implement pagination in Next.js 13 using the App Router and Server Components

How to implement pagination in Next.js 13 using the App Router and Server Components


1

I started building a Next.js frontend for a WordPress website, using the App Router and GraphQL plugin to fetch data from WordPress. The homepage is all set up to display the latest posts, and it’s a server component.

import PostCard from "./components/post";

import styles from './page.module.scss'

interface Post {
    title: string;
    date: string;
    excerpt: string;
    featuredImage: {
        node: {
          sourceUrl: string;
        };
    };
    categories: {
        edges: {
            node: {
                name: string;
            };
        }[];
    };
    slug: string;
    author: {
        node: {
            name: string;
        };
    };
}

async function getPosts(cursor: string): Promise<Post[]> {
    const endpoint = process.env.WORDPRESS_API_URL || '';

    const response = await fetch(endpoint, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            query: `
                query GetPosts($cursor: String!) {
                    posts(first: 10, after: $cursor) {
                        nodes {
                            title
                            date
                            excerpt
                            featuredImage {
                                node {
                                    sourceUrl
                                }
                            }
                            categories {
                                edges {
                                    node {
                                        name
                                    }
                                }
                            }
                            slug
                            author {
                                node {
                                    name
                                }
                            }
                        }
                    }
                }
            `,
            variables: {
                cursor: cursor,
            },
        }),
    });

    const data = await response.json();

    return data.data.posts.nodes;
}

export default async function HomePage() {

    const posts = await getPosts("");

    return (
        <div>
            <div className={styles.postsList}>
                    {posts.map((post) => (
                        <PostCard post={post} key={post.slug}/>
                    ))}
            </div>
            <button onClick={ }>Load More</button>
        </div>
    );
}

The challenge is loading the next batch of posts right after this cursor (which is ID of the last post I loaded by GraphQL) without using React’s useState or useEffect hooks, as these do no work on server components.

How can I make the "Load More" button work with server components?

I’ve tried to tackle pagination using query strings, but that plan fizzled out because GraphQL’s WordPress plugin exclusively works with cursor-based pagination.

Share

New contributor

Ioan is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

1 Answer
1

Reset to default


0

Server components and client components can be combined in the same component tree, which is what you should be doing here: Separate the server part from the client part in two separated components.

Most of what you posted is the server part, and remains mostly the same, with some minor changes to use the client component inside HomePage:

export default async function HomePage() {

  const posts = await getPosts("");

  return (
    <div>
      <div className={styles.postsList}>
        { posts.map((post) => (
          <PostCard post={ post } key={ post.slug } />
        )) }
      </div>
      
      <MorePosts>
    </div>
  );

}

Then, you’ll have the client component, which would look something like this:

'use client'

export const MorePosts = () => {

  // This is just some placeholder / pseudo-code for whatever custom logic
  // you need to fetch these:

  const { morePosts, loadMorePosts, areMorePostsLoading } = useMorePosts();

  return (
    <>
      <div className={ styles.morePostsList }>
        { morePosts.map((post) => (
          <PostCard post={ post } key={ post.slug } />
        )) }
      </div>

      <button
        onClick={ loadMorePosts }
        disabled={ areMorePostsLoading }>
        Load More
      </button>
    </>
  )
}

Note useMorePosts() represents a custom hook that probably uses useEffect and useState plus fetch, axios or some other client-side request library to load additional posts in pages / batches and store them, plus manage the associated status (loading, error, cursor position…).

Share



Not the answer you're looking for? Browse other questions tagged

or ask your own question.

Leave a Reply

Your email address will not be published. Required fields are marked *