Luciano Serruya Aloisi

Software developer from Trelew, Chubut, Argentina 🇦🇷

Building a full-stack app with Next.js - Pagination

Hey there! This is the fourth (and maybe last) part of a series about building a full-stack app with Next.js and deploying it to the cloud using GCP and AWS (we are on a budget). Our app currently allows us to add, edit, delete, and list posts, using a PostgreSQL database.

Let's suppose you created a lot of posts in your app, up to a hundred. If you go to the index page of your app, it will display every single one of them all at once. This is not something good to have, as it becomes quite hard to navigate through your app. In this part of the series we are going to add pagination, which means that a row of buttons will appear at the bottom of the page to go to the previous/next one. As we are using a ORM that provides us some functionalities to implement this feature, it shouldn't require a lot of work.

This is how our pagination mechanism will work:

  • If we visit /, the first 10 post will be displayed
  • Our pages will work on a 0-based index, so page 0 will be the first one.
  • To get another page, we will need to indicate it as a query parameter (for example /?page=1 will display posts 11-20)
  • To change the page size (default is 10 post per page), we will need to indicate it via the count query parameter.

So, if we visit the URL /?page=2&count=20, we should see posts 41-60.

We are going to add our pagination logic right into the getServerSideProps function in pages/index.tsx

// pages/index.tsx

export async function getServerSideProps(context: GetServerSidePropsContext) {
  // `page` defaults to "0" and `count` to "10" if not sent
  const { page = "0", count = "10" } = context.query;
  const pageNumber = parseInt(page as string);
  const countNumber = parseInt(count as string);
  const postsToSkip = countNumber * pageNumber;

  const conn = await getOrCreateConnection();
  const postRepo = conn.getRepository<Post>("Post");

  const postsCount = await postRepo.count();
  const posts = (
    await postRepo.find({ take: countNumber, skip: postsToSkip })
  ).map(p => JSON.stringify(p));

  const previousPage = pageNumber > 0 ? String(pageNumber - 1) : "";
  // We don't want to indicate there is a next page if it'll be a blank page
  const nextPage =
    postsToSkip + countNumber < postsCount ? String(pageNumber + 1) : "";

  return {
    props: {
      msg: "Hello world!",
      posts,
      pagination: { previousPage, nextPage }
    }
  };
}

We are now using some of the query options TypeORM offers us to only fetch those posts that we want, using take (similar to LIMIT in SQL) and skip (used to tell TypeORM how many posts we want to skip).

Our pagination logic is now ready, we now have to display some buttons to navigate back and forth. For that, we are going to create a different component.

// components/Pagination.tsx

type Props = {
  previousPage: string;
  nextPage: string;
};

type PaginationButtonProps = {
  page: string;
};

const PreviousPageButton: React.FC<PaginationButtonProps> = ({ page }) => {
  return (
    <a href={`?page=${page}`}>
      <button className="border border-teal-500 text-teal-500 block rounded font-bold pl-4 pr-2 py-4 flex items-center">
        <svg
          className="h-5 w-5 mr-2 fill-current"
          version="1.1"
          id="Layer_1"
          x="0px"
          y="0px"
          viewBox="-49 141 512 512"
        >
          <path
            id="XMLID_10_"
            d="M438,372H36.355l72.822-72.822c9.763-9.763,9.763-25.592,0-35.355c-9.763-9.764-25.593-9.762-35.355,0 l-115.5,115.5C-46.366,384.01-49,390.369-49,397s2.634,12.989,7.322,17.678l115.5,115.5c9.763,9.762,25.593,9.763,35.355,0 c9.763-9.763,9.763-25.592,0-35.355L36.355,422H438c13.808,0,25-11.193,25-25S451.808,372,438,372z"
          ></path>
        </svg>
        <span className="hidden md:block">Previous page</span>
      </button>
    </a>
  );
};

const NextPageButton: React.FC<PaginationButtonProps> = ({ page }) => {
  return (
    <a href={`?page=${page}`}>
      <button className="border border-teal-500 bg-teal-500 text-white block rounded font-bold pl-2 pr-4 py-4 ml-2 flex items-center">
        <span className="hidden md:block">Next page</span>
        <svg
          className="h-5 w-5 ml-2 fill-current"
          id="Layer_1"
          x="0px"
          y="0px"
          viewBox="-49 141 512 512"
        >
          <path
            id="XMLID_11_"
            d="M-24,422h401.645l-72.822,72.822c-9.763,9.763-9.763,25.592,0,35.355c9.763,9.764,25.593,9.762,35.355,0
                l115.5-115.5C460.366,409.989,463,403.63,463,397s-2.634-12.989-7.322-17.678l-115.5-115.5c-9.763-9.762-25.593-9.763-35.355,0
                c-9.763,9.763-9.763,25.592,0,35.355l72.822,72.822H-24c-13.808,0-25,11.193-25,25S-37.808,422-24,422z"
          />
        </svg>
      </button>
    </a>
  );
};

export const Pagination: React.FC<Props> = ({ previousPage, nextPage }) => {
  const previousPageButton = previousPage
    ? PreviousPageButton({ page: previousPage })
    : null;
  const nextPageButton = nextPage ? NextPageButton({ page: nextPage }) : null;
  return (
    <section className="flex justify-end m-4">
      {previousPageButton}
      {nextPageButton}
    </section>
  );
};

(it's a lot of code, but we added three new components: Pagination, PreviousPageButton, and NextPageButton, although we are only exporting the first one)

Pagination buttons taken from here.

To use our brand new pagination component, we just need to import it in our pages/index.tsx file and add it to its markup

// pages/index.tsx

export default function Home({
  posts,
  msg,
  pagination
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  const postObjs = posts.map(p => JSON.parse(p) as Post);

  return (
    <Fragment>
      <h1 className="m-4 text-center text-4xl text-red-500">{msg}</h1>
      <PostList posts={postObjs} />
      <Pagination {...pagination} />
    </Fragment>
  );
}

We can do {...pagination} because pagination has the same fields that Pagination component requires as props.

Deployment

Same as last part. Use our really handy npm-scripts to build and deploy our app

npm run gcp:build && npm run gcp:deploy

Hope you liked it!

🐦 @LucianoSerruya

📧 lucianoserruya (at) gmail (dot) com