DEV Community

Ioannis Potouridis
Ioannis Potouridis

Posted on • Edited on

The only pagination you'll ever need 1️⃣2️⃣3️⃣4️⃣5️⃣ (a React hook example)

There are a lot of things to take into consideration when creating a table pagination component.

I never had the chance to use a ready one, but I assume that every pagination component or hook needs at least these in order to work.

interface UsePaginationProps {
  /** Total number of rows */
  count: number;
  /** The current page */
  page: number;
  /** How many rows per page should be visible */
  rowsPerPage: number;
  /** What are the provided options for rowsPerPage */
  rowsPerPageOptions: number[];
}

Then, it typically renders a dropdown in order to be able to choose one of the rowsPerPageOptions, the pages as links, with the current one usually highlighted and finally some buttons that navigate to first or last, previous or next page.

I don't care about the UI so I'll create a hook that says what I should (makes sense to) render at a given state e.g:

const state: UsePaginationProps = {
  count: 27,
  page: 2,
  rowsPerPage: 10,
  rowsPerPageOptions: [10, 30, 50]
}

I have 27 rows in total, I'm currently at the second page and I am viewing 10 rows. I should see 3 page options. I should also have a next and a previous button but I don't need a first or last button because I'm displaying all of the available page options at the moment ([1, 2, 3]: 1 is the first etc.).

I'll prepare the hook like this.

function usePagination({
  count,
  page,
  rowsPerPage,
  rowsPerPageOptions
}: UsePaginationProps) {
  return {};
}

export default usePagination;

I need to find out how many pages do I have to start with.

Given my current information, I can calculate that by dividing the number of total rows I have by the number of rows I am showing by page. It should look something like this.

const pageCount = Math.ceil(count / rowsPerPage);

The reason I have to round it upwards is simply because I don't want to miss any left overs.

With that, the hook should then be like this.

import { useMemo } from 'react';

function usePagination({
  count,
  page,
  rowsPerPage,
  rowsPerPageOptions
}: UsePaginationProps) {
  const pageCount = useMemo(() => {
    return Math.ceil(count / rowsPerPage);
  }, [count, rowsPerPage]);

  return { pageCount };
}

export default usePagination;

I'll continue by calculating the adjacent pages.

By the way, this example will always display 5 pages or less and the current page will always be in the middle unless I have reached each end (offset from center inc.).

To begin with, I'll create all the pages 1, 2, 3... n by writing the next line:

const value = Array.from(new Array(pageCount), (_, k) => k + 1);

I want to return the current page and the adjacent two on both sides.

With value containing all my pages, I can accomplish that with value.slice(page - 3, page + 2);

And the complete calculation (inc. checking the offsets) looks like this:

import { useMemo } from 'react';

function usePagination({
  count,
  page,
  rowsPerPage,
  rowsPerPageOptions
}: UsePaginationProps) {
  const pageCount = useMemo(() => {
    return Math.ceil(count / rowsPerPage);
  }, [count, rowsPerPage]);

  const pages = useMemo(() => {
    const value = Array.from(new Array(pageCount), (_, k) => k + 1);

    if (page < 3) {
      return value.slice(0, 5);
    }

    if (pageCount - page < 3) {
      return value.slice(-5);
    }

    return value.slice(page - 3, page + 2);
  }, [page, pageCount]);

  return { pageCount, pages };
}

export default usePagination;

I have all the information I need in order to render or not the navigation buttons but let's add the show rules and return them, why not?

import { useMemo } from 'react';

function usePagination({
  count,
  page,
  rowsPerPage,
  rowsPerPageOptions
}: UsePaginationProps) {
  const pageCount = useMemo(() => {
    return Math.ceil(count / rowsPerPage);
  }, [count, rowsPerPage]);

  const pages = useMemo(() => {
    const value = Array.from(new Array(pageCount), (_, k) => k + 1);

    if (page < 3) {
      return value.slice(0, 5);
    }

    if (pageCount - page < 3) {
      return value.slice(-5);
    }

    return value.slice(page - 3, page + 2);
  }, [page, pageCount]);

  const showFirst = useMemo(() => {
    return page > 3;
  }, [page]);

  const showNext = useMemo(() => {
    return pageCount - page > 0;
  }, [page, pageCount]);

  const showLast = useMemo(() => {
    return pageCount - page > 2;
  }, [page, pageCount]);

  const showPages = useMemo(() => {
    return pages.length !== 1;
  }, [pages.length]);

  const showPagination = useMemo(() => {
    return count >= Math.min(...rowsPerPageOptions);
  }, [count, rowsPerPageOptions]);

  const showPrevious = useMemo(() => {
    return page > 1;
  }, [page]);


  return {
    pages,
    showFirst,
    showNext,
    showLast,
    showPages,
    showPagination,
    showPrevious
  };
}

export default usePagination;

showPages: I don't need to display pages if I have a single page.

showPagination: I don't need to show the pagination if I have less rows than my minimum rowsPerPage option.

With that, if I use the example state like this:

const pagination = usePagination({
  count: 27,
  page: 2,
  rowsPerPage: 10,
  rowsPerPageOptions: [10, 30, 50]
});

I should get what I expect to see:

{
  "pages": [
    1,
    2,
    3
  ],
  "showFirst": false,
  "showNext": true,
  "showLast": false,
  "showPages": true,
  "showPagination": true,
  "showPrevious": true
}

Top comments (2)

Collapse
 
coconutnegro profile image
Hendra Widjaja

Do you have it in Github as source code?

Collapse
 
potouridisio profile image
Ioannis Potouridis • Edited

No, I'm afraid not. I used this logic in a private repository in a pagination component. I thought the logic could be extracted as a hook and decided to make a post about it. All the source code exists in the post though. 😉