How to Create Pagination Component in NextJS and Tailwind CSS
Jasser Mark Arioste
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.
You can modify the query parameters to test it out and this will give you different results. For example
- /products?page=1&perPage=16
- /products?page=2
- /products?page=3&perPage=8
- /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:
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