Build Advanced React Pagination Component | React Table Pagination
Build Advanced React Pagination Component | React Table Pagination

In one of the previous posts we implemented simple pagination inside React. We have a list of our pages, we can jump between them and current page is being highlighted.

Before after

A lot of people wrote that it is too simple and it is suitable only for small applications. This is why in this post we want to build several things. First of all we want too implement here previous, next, first and last buttons and disable them when needed. Secondly we will limit the amount of pages that we show in order to make it scalable for the big project.

What we have

So first of all let's look on our existing pagination and how it works.

const App = () => {
  const [currentPage, setCurrentPage] = useState(1);
  return (
    <div className="container">
      <Pagination
        currentPage={currentPage}
        total={500}
        limit={20}
        onPageChange={(page) => setCurrentPage(page)}
      />
    </div>
  );
};

This is how we use it. It is completely stateless without any state inside. For the outside we provide currentPage from the outside state as well as total and limit to calculate how many pages we need to render.

import "./pagination.css";
import classNames from "classnames";

const range = (start, end) => {
  return [...Array(end).keys()].map((el) => el + start);
};

const PaginationItem = ({ page, currentPage, onPageChange }) => {
  const liClasses = classNames({
    "page-item": true,
    active: page === currentPage,
  });
  return (
    <li className={liClasses} onClick={() => onPageChange(page)}>
      <span className="page-link">{page}</span>
    </li>
  );
};

const Pagination = ({ currentPage, total, limit, onPageChange }) => {
  const pagesCount = Math.ceil(total / limit);
  const pages = range(1, pagesCount);
  return (
    <ul className="pagination">
      {pages.map((page) => (
        <PaginationItem
          page={page}
          key={page}
          currentPage={currentPage}
          onPageChange={onPageChange}
        />
      ))}
    </ul>
  );
};
export default Pagination;

Our Pagination component is also relatively small. We calculate pagesCount and create an array of pages which we render on the screen. Also we have PaginationItem which adds classes and click event.

Previous and next buttons

It is extremely easy to implement previous and next buttons because we have a reusable PaginationItem component.

<PaginationItem
  page="Prev"
  currentPage={currentPage}
  onPageChange={() => onPageChange(currentPage - 1)}
/>
{pages.map((page) => (
  <PaginationItem
    page={page}
    key={page}
    currentPage={currentPage}
    onPageChange={onPageChange}
  />
))}

Before our mapping for pages we render a single PaginationItem. Instead of the number we provide text Prev inside. This is totally fine as we just render it as it is inside an item. But we need to change our onPageChange because we want to jump to the previous page and not to some specific page.

{pages.map((page) => (
  <PaginationItem
    page={page}
    key={page}
    currentPage={currentPage}
    onPageChange={onPageChange}
  />
))}
<PaginationItem
  page="Next"
  currentPage={currentPage}
  onPageChange={() => onPageChange(currentPage + 1)}
/>

Exactly the same logic we implement with our Next button.

Prev Next

As you can see we successfully implemented our previous and next buttons.

First and last buttons.

The some logic we want to apply to first and last button but we need to jump to the last and first page accordingly.

<PaginationItem
  page="First"
  currentPage={currentPage}
  onPageChange={() => onPageChange(1)}
  isDisabled={isFirstPage}
/>
...
<PaginationItem
  page="Last"
  currentPage={currentPage}
  onPageChange={() => onPageChange(pages.length)}
  isDisabled={isLastPage}
/>

In the first case we jump directly to page 1. In the second case we use pages.length to jump to the last page.

But now we have a problem. When we are on the first of last page we can still click previous and next and we go outside of our limit.

In order to avoid that we want to disable our buttons when we are on the first of last page.

const isFirstPage = currentPage === 1;
const isLastPage = currentPage === pagesCount;
return (
  <ul className="pagination">
    <PaginationItem
      page="First"
      currentPage={currentPage}
      onPageChange={() => onPageChange(1)}
      isDisabled={isFirstPage}
    />
    <PaginationItem
      page="Prev"
      currentPage={currentPage}
      onPageChange={() => onPageChange(currentPage - 1)}
      isDisabled={isFirstPage}
    />
    ...
    <PaginationItem
      page="Next"
      currentPage={currentPage}
      onPageChange={() => onPageChange(currentPage + 1)}
      isDisabled={isLastPage}
    />
    <PaginationItem
      page="Last"
      currentPage={currentPage}
      onPageChange={() => onPageChange(pages.length)}
      isDisabled={isLastPage}
    />
  </ul>
);
};

Disabling buttons

As you can see we disable first and previous buttons when we are on the first page and next and last when we are on the last page.

Scalable pagination

Our next big feature here is to render less elements to avoid problem when we have lots of pages.

  • If we are in the middle of pagination we render several pages left and right
  • If we are at the beginning we render first page and several pages right
  • If we are at the end we render last page and several pages left

This is why I want to create a new function which will calculate the needed range.

const getPagesCut = ({ pagesCount, pagesCutCount, currentPage }) => {
  const ceiling = Math.ceil(pagesCutCount / 2);
  const floor = Math.floor(pagesCutCount / 2);

  if (pagesCount < pagesCutCount) {
    return { start: 1, end: pagesCount + 1 };
  } else if (currentPage >= 1 && currentPage <= ceiling) {
    return { start: 1, end: pagesCutCount + 1 };
  } else if (currentPage + floor >= pagesCount) {
    return { start: pagesCount - pagesCutCount + 1, end: pagesCount + 1 };
  } else {
    return { start: currentPage - ceiling + 1, end: currentPage + floor + 1 };
  }
};

Here we get total pagesCount. pagesCutCount means how many pages we want to render on the screen and currentPage we need for calculations.

We have several cases:

  • First if happens when our total amount of pages is smaller than amount that we want to render. Then we take total amount as end
  • Second if happens when we are at the beginning of pagination
  • Third if happens when we are at the end of pagination
  • At last if happens when we are at the middle

As you can see we always used +1. We do that because of how our range function works. It doesn't include our end which is why we must increase it on 1.

Now we must generate our pages using which getPagesCut function.

const Pagination = ({ currentPage, total, limit, onPageChange }) => {
  const pagesCount = Math.ceil(total / limit);
  const pagesCut = getPagesCut({ pagesCount, pagesCutCount: 5, currentPage });
  const pages = range(pagesCut.start, pagesCut.end);
  const isFirstPage = currentPage === 1;
  const isLastPage = currentPage === pagesCount;
}

As you can see we generated pagesCut property and generated our range of pages from it.

Finished project

And actually if you are interested to learn how to build real React project from start to the end make sure to check my React hooks course.

📚 Source code of what we've done