Last updated: Dec 26, 2022

How to create pagination in a react app from scratch with Typescript

Pagination is an important component whether you're building a dynamic web app to display items in a table or blog posts on multiple pages. Creating pagination from scratch is easy and to make things simple to understand I have broken up the logic/code into small steps in this tutorial. So lets begin and find out how it works.

Create a component to return an anchor tag for navigating between pages.

The first component that we are going to create is for displaying the page numbers. It is also going to help us navigate between different pages in our web app.

Let's start by adding the following code to the file "PageLink.tsx" in your "components" folder.

import { HTMLProps } from 'react';
import './PageLink.css';

type Props = HTMLProps<HTMLAnchorElement> & { active?: boolean };

function classNames(...classes: any) {
  return classes.filter(Boolean).join(' ');
}

export default function PageLink({
  className,
  active,
  disabled,
  children,
  ...rest
}: Props) {
  const customClassName = classNames(
    active && 'page-link-active',
    disabled && 'page-link-disabled',
    'page-link',
    className,
  );

  if (disabled) {
    return <span className={customClassName}>{children}</span>;
  }

  return (
    <a
      className={customClassName}
      aria-current={active ? 'page' : undefined}
      {...rest}
    >
      {children}
    </a>
  );
}

On line 4, we are passing a custom active attribute of boolean type to the component props. On line 6, we create a function to apply classes from the "PageLink.css" file based on the active or disabled state of the page number.

Here, if the disabled attribute is true then a span element is returned instead.

Next, we are going to add styling to the anchor element. Create a new file with name "PageLink.css" and add the following to it.

.page-link {
  position: relative;
  display: inline-flex;
  border: 1px solid #dee2e6;
  background-color: #ffffff;
  padding: 10px 15px;
  color: #0d6efd;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
    border-color 0.15s ease-in-out;
  cursor: pointer;
}

.page-link:first-child {
  width: auto;
  border-top-left-radius: 5px;
  border-bottom-left-radius: 5px;
}

.page-link:last-child {
  width: auto;
  border-top-right-radius: 5px;
  border-bottom-right-radius: 5px;
}

.page-link:not(:first-child) {
  margin-left: -1px;
}

.page-link:hover,
.page-link:focus {
  color: #0a58ca;
  background-color: #e9ecef;
}

.page-link:focus {
  z-index: 3;
}

.page-link-active {
  z-index: 2;
  color: #ffffff;
  border-color: #0d6efd;
  background-color: #0d6efd;
}

.page-link-disabled {
  color: #6c757d;
  pointer-events: none;
}

Create a component to display page numbers with previous and next buttons.

We are now ready to create our pagination component to display the page numbers. This component uses the PageLink component that we created above and displays the page numbers by mapping items available in an array returned by the paginator function.

Lets create another file with name "Pagination.tsx" and write the following code to it.

import PageLink from './PageLink';
import { paginator } from '../lib/paginator';
import './Pagination.css';

type Props = {
  currentPage: number;
  lastPage: number;
  maxLength: number;
  setCurrentPage: (page: number) => void;
};

export default function Pagination({
  currentPage,
  lastPage,
  maxLength,
  setCurrentPage,
}: Props) {
  const pageNums = paginator(currentPage, lastPage, maxLength);

  return (
    <nav className='pagination' aria-label='Pagination'>
      <PageLink
        disabled={currentPage === 1}
        onClick={() => setCurrentPage(currentPage - 1)}
      >
        Previous
      </PageLink>
      {pageNums.map((pageNum, idx) => (
        <PageLink
          key={idx}
          active={currentPage === pageNum}
          disabled={isNaN(pageNum)}
          onClick={() => setCurrentPage(pageNum)}
        >
          {!isNaN(pageNum) ? pageNum : '...'}
        </PageLink>
      ))}
      <PageLink
        disabled={currentPage === lastPage}
        onClick={() => setCurrentPage(currentPage + 1)}
      >
        Next
      </PageLink>
    </nav>
  );
}

On line 2, you may notice an import statement with a function named paginator. This function returns an array that we can map and we will later find out how it works.

The code above is straightforward and easy to understand if you have some experience with React. Here, one important thing to note is the isNan accessor function that checks for a valid number and shows an ellipses otherwise.

We are using NaN (Not a number) value just for the sake of simplicity. I think using number values in the array instead of a string will help us avoid writing a conversion logic to convert the value from string to number in this case.

Styles the above component's container by creating a new file named "Pagination.css" with the following code.

.pagination {
  display: flex;
  flex-wrap: wrap;
}

Moving forward, let's understand how the paginator function works and how it returns an array that we need to map our page numbers.

Create a paginator function and write pagination logic

Create a new file with name "paginator.ts" in a separate folder "lib" and write the following code in it.

export function paginator(
  currentPage: number,
  lastPage: number,
  maxLength: number,
) {
  const arr: Array<number> = [];

  if (lastPage <= maxLength) {
    for (let i = 1; i <= lastPage; i++) {
      arr.push(i);
    }
  } else {
    // ...
  }

  return arr;
}

We are going to use a few if and else statements to do the job. The function above accepts three parameters i.e. currentPage, lastPage, and maxLength. The first parameter, currentPage, is the current page open at client side.

Similarly, there is lastPage and maxLength which is the maximum number of items or page numbers to display at a time in your pagination user interface.

On line 6 in the above code block, we create a variable that holds an array with name arr. This is the array that is returned and mapped in our "Pagination.tsx" component.

The first condition checks if the maximum length that we want is equal or more than the total number of pages. Here, there is no point of showing an ellipses as we have all the pages that can fit in the UI. So, we simply run a for loop to add and return the page numbers in the array.

Show ellipses in the middle

In the next condition, if the maximum length is less than the total number of pages, we divide the pages and fill the array.

if (lastPage <= maxLength) {
  // ...
} else {
  const itemsToShow = Math.floor(maxLength / 2); // items to show on side of ellipses

  let itemInMiddle = Math.floor(maxLength / 2);

  if (currentPage <= itemInMiddle || currentPage > lastPage - itemsToShow) {
    // Items to map on left
    for (let l = 1; l <= itemsToShow; l++) {
      arr.push(l);
    }

    // items to map on right
    const firstItemToShowOnRight = lastPage - itemsToShow;
    for (let r = firstItemToShowOnRight; r <= lastPage; r++) {
      if (r === firstItemToShowOnRight) {
        arr.push(NaN);
      } else {
        arr.push(r);
      }
    }
  }

  // ...
}

On line 4, we create a variable, itemsToShow, to hold the number of items to show on either side of the ellipses if our page numbers exceed the maximum length. We also create another variable with name itemInMiddle that calculates the center of the array or page number that falls in middle. This is helpful to figure out what to show on the left or right side of the array.

On line 8, the condition checks if the current page is equal to item in middle, falls on the left side of the array or is on the far end. Then, we run two for loops to display page numbers both on the left and right.

On line 17, when mapping on right, we assign a NaN to the index where we are showing the first item to right of center. This will show an ellipses in the very center of the array and display the page numbers on both sides.

When you move to the center or the fourth page, e.g. if maxLength is 7, you will see an ellipses instead of current page number. To solve this problem, we can show two ellipses to the left and right witch the current page in the middle.

Show two ellipses and the current page in middle

else {
  // ...

  if (currentPage <= itemInMiddle || currentPage > lastPage - itemsToShow) {
    // ...
  }

  // we are now going to show two ellipses to the left and right and our current page number in middle
  if (currentPage > itemInMiddle && currentPage <= lastPage - itemsToShow) {
    // items to show on left
    for (let l = 1; l <= itemsToShow; l++) {
      if (l === itemsToShow - 1) {
        arr.push(NaN);
      } else if (l === itemsToShow) {
        arr.push(currentPage - 1);
      } else {
        arr.push(l);
      }
    }

    // Item to show in middle
    arr.push(currentPage);

    // items to map on right
    const firstItemToShowOnRight = lastPage - itemsToShow + 1;
    for (let r = firstItemToShowOnRight; r <= lastPage; r++) {
      if (r === firstItemToShowOnRight + 1) {
        arr.push(NaN);
      } else if (r === firstItemToShowOnRight) {
        arr.push(currentPage + 1);
      } else {
        arr.push(r);
      }
    }
  }
}

On line 9, the condition checks if the current page falls both after the middle and before items that are to be mapped on the right.

Here, we run two for loops again to map items both on left and right while adding the current page as the center of the array. Also, a NaN is assigned to the last and first position of our items that are to be shown on either side.

Using the component with react state

The pagination component is now ready to use with any module in your react app. The following snippet shows how to import and use it with react state.

import { useState } from 'react';
import Pagination from '../components/Pagination';
import './App.css';

export default function App() {
  const [currentPage, setCurrentPage] = useState(1);
  return (
    <div>
      <h1>React Pagination</h1>
      <Pagination
        currentPage={currentPage}
        setCurrentPage={setCurrentPage}
        lastPage={20}
        maxLength={8}
      />
    </div>
  );
}

To view the live example, visit React Pagination.