ReactHustle

How to Create Pagination Component in NextJS and Tailwind CSS

Jasser Mark Arioste

Jasser Mark Arioste

How to Create Pagination Component in NextJS and Tailwind CSS

Hello, hustlers! In this tutorial, you'll learn how to create a Pagination component in NextJS. We're going to use TailwindCSS for styling and code using Typescript.

Introduction #

Sometimes, we need to implement a pagination component for our application. But I think for react applications, one of the best practices is to save the page state in the URL parameters. This is so that each page is easily bookmarkable and crawlable for search engines.

Fortunately, NextJS provides a way to easily access query parameters using the useRouter() hook. We're going to the useRouter hook to save and change the state of our page.

Step 0 - Project Setup #

To speed up the project setup, I created a NextJS template with TailwindCSS pre-installed. Just run the following command to create a brand new NextJS project:

npx create-next-app -e https://github.com/jmarioste/next-tailwind-starter-2 next-js-pagination-tutorial

Once everything is installed, run the following commands to start the local dev server:

cd next-js-pagination-tutorial
yarn dev

Step 1 - Creating the <ProductsPage/> Component #

Let's assume you need to build a product catalog for an e-commerce brand. We'll use DummyJSON API as our backend since they have good mock data available for use. 

First, we're going to build the products page, then add a <Pagination/> component afterward.

Let's install a custom utility hook called usehooks-ts. This library is a collection of custom hooks and it contains the very useful useFetch() hook that can help us when fetching data. 

yarn add usehooks-ts
1

After that, create the file pages/products.tsx. Examine the code below, I've added comments to explain each step.

// pages/products.tsx
import { useRouter } from "next/router";
import { useFetch } from "usehooks-ts";
import Image from "next/image";

// Line 7-18 Define the response types. We derive this from DummyJSON api docs
type Product = {
  id: number;
  title: string;
  price: number;
  thumbnail: string;
};
type Response = {
  products: Product[];
  total: number;
  skip: number;
  limit: number;
};
const ProductsPage = () => {
  // 21-25 parse the page and perPage  from router.query
  const router = useRouter();
  const query = router.query;
  const page = (query.page as string) ?? "1";
  const perPage = (query.perPage as string) ?? "12";

  // Lines 27-29: Define limit and skip which is used by DummyJSON API for pagination
  const limit = perPage;
  const skip = (parseInt(page) - 1) * parseInt(limit);
  const url = `https://dummyjson.com/products?limit=${limit}&skip=${skip}&select=title,price,thumbnail`;

  // Line 32:  use the useFetch hook to get the products
  const { data } = useFetch<Response>(url);

  return (
    // we use tailwindCSS classes to create a decent product grid
    <div className="mx-auto container">
      {!data && <div>Loading...</div>}
      <div className="grid grid-cols-12 gap-4 p-4">
        {data?.products?.map((product) => {
          // render each product
          return (
            <a
              key={product.id}
              className="col-span-3 shadow-md rounded-md block overflow-hidden"
              href={`/products/${product.id}`}
            >
              <div className="relative aspect-video ">
                <Image
                  src={product.thumbnail}
                  fill
                  className="object-cover"
                  alt={product.title}
                />
              </div>
              <div className="p-4 flex justify-between">
                <h2 className="text-xl font-semibold">{product.title}</h2>
                <p>${product.price.toFixed(2)}</p>
              </div>
            </a>
          );
        })}
      </div>
    </div>
  );
};
export default ProductsPage;
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566

Once you're done, you can go to /products route from your browser and you should have the following output or similar.

NextJS pagination component step 1

You can modify the query parameters to test it out and this will give you different results. For example

  1. /products?page=1&perPage=16
  2. /products?page=2
  3. /products?page=3&perPage=8
  4. /products?page=3&perPage=24

With this, you're halfway done since it already has pagination functionality using the query parameters. We just need to build the pagination component to add controls for the user.

Step 2 - Creating the <Pagination/> Component #

Rather than creating the logic from scratch, and to easily help us track the states of our <Pagination/> component, we're going to use a custom hook from the @lucasmogari/react-pagination package. This package is a headless pagination package meaning, we'll be able to style and customize our pagination component as much as we want.

We'll also install the classnames package to easily toggle TailwindCSS classes.

yarn add @lucasmogari/react-pagination classnames
1

Once that's done, create the file components/Pagination.tsx. I've added comments explaining how to use the usePagination hook. 

// components/Pagination.tsx
import usePagination from "@lucasmogari/react-pagination";
import cn from "classnames";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { memo, PropsWithChildren, ReactNode } from "react";

type Props = {
  page: number;
  itemCount: number;
  perPage: number;
};

const Pagination = ({ page, itemCount, perPage }: Props) => {
  // use the usePagination hook
  // getPageItem - function that returns the type of page based on the index.
  // size - the number of pages
  const { getPageItem, totalPages } = usePagination({
    totalItems: itemCount,
    page: page,
    itemsPerPage: perPage,
    maxPageItems: 7,
  });

  const firstPage = 1;
  // calculate the next page
  const nextPage = Math.min(page + 1, totalPages);
  // calculate the previous page
  const prevPage = Math.max(page - 1, firstPage);
  // create a new array based on the total pages
  const arr = new Array(totalPages);

  return (
    <div className="flex gap-2 items-center">
      {[...arr].map((_, i) => {
        // getPageItem function returns the type of page based on the index.
        // it also automatically calculates if the page is disabled.
        const { page, disabled,current } = getPageItem(i);

        if (page === "previous") {
          return (
            <PaginationLink page={prevPage} disabled={disabled} key={page}>
              {`<`}
            </PaginationLink>
          );
        }

        if (page === "gap") {
          return <span key={`${page}-${i}`}>...</span>;
        }

        if (page === "next") {
          return (
            <PaginationLink page={nextPage} disabled={disabled} key={page}>
              {`>`}
            </PaginationLink>
          );
        }

        return (
          <PaginationLink active={current } key={page} page={page!}>
            {page}
          </PaginationLink>
        );
      })}
    </div>
  );
};

type PaginationLinkProps = {
  page?: number | string;
  active?: boolean;
  disabled?: boolean;
} & PropsWithChildren;

function PaginationLink({ page, children, ...props }: PaginationLinkProps) {
  const router = useRouter();
  const query = router.query;

  // we use existing data from router query, we just modify the page.
  const q = { ...query, page };
  return (
    <Link
      // only use the query for the url, it will only modify the query, won't modify the route.
      href={{ query: q }}
      // toggle the appropriate classes based on active, disabled states.
      className={cn({
        "p-2": true,
        "font-bold text-indigo-700": props.active,
        "text-indigo-400": !props.active,
        "pointer-events-none text-gray-200": props.disabled,
      })}
    >
      {children}
    </Link>
  );
}
export default memo(Pagination);
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798

In the code above, we use the usePagination hook to get the state of the pagination component. It's up to render the elements and add appropriate CSS classes based on the state. We can easily customize the look and feel, and also the behavior of our pagination component.

To use this component, let's go back to pages/products.tsx:

// pages/products.tsx
// ...
import Pagination from "../components/Pagination";

const ProductsPage = () => {
  // ...
  return (
    // we use tailwindCSS classes to create a decent product grid
    <div className="mx-auto container">
      <Pagination
        page={parseInt(page)}
        perPage={parseInt(perPage)}
        itemCount={data?.total ?? 0}
      />
      {!data && <div>Loading...</div>}
      ...
    </div>
  );
};
export default ProductsPage;
1234567891011121314151617181920

Once that's done, you should have the following output:

Next JS Pagination Step 3 output.

Full Code and Demo #

The full code and demo are available on Stackblitz: NextJS Pagination Tutorial. Make sure to navigate to the /products route.

Or you can create a next js app locally by running the command:

npx create-next-app -e https://github.com/jmarioste/nextjs-pagination-tutorial

Conclusion #

You learned how to use useRouter and usePagination hooks to create a very flexible and customizable pagination component. 

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Resources #

Credits: Image by Andreas from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle