DEV Community

Cover image for Pagination in Javascript and React, with a custom usePagination() hook
damilola jerugba
damilola jerugba

Posted on • Edited on • Originally published at damiisdandy.com

Pagination in Javascript and React, with a custom usePagination() hook

This guide is to help you understand the concept of pagination and how to implement it in react, the concepts in this tutorial can be applied to any javascript project.

๐Ÿคจ What is Pagination?

Pagination is the process of separating print or digital content into discrete pages. For print documents and some online content, pagination also refers to the automated process of adding consecutive numbers to identify the sequential order of pages.

Pagination component

Concept behind it? ๐Ÿ’ป

Let's say you have a total of 6 items on a page, and you want to only display 3 items at a time (per page). This means we are going to have a total of 2 pages, and if we want to display 2 items per page this means a total of?? you guessed it! 3 pages.

Illustration of pagination concept

This formular is rather simple:
totalPages = totalContent / contentPerPage

Implementing it in Javascript (.slice()) ๐Ÿ”ช

Calculating the content per page is rather easy, but how do we display certain content based on what page we are on? We simply need to understand the relationship between the page and the index of our content. Let first understand the .slice() Array method.

The slice() method returns a shallow copy of a portion of an array into a new array object selected from start to end (end not included) where start and end represent the index of items in that array. The original array will not be modified.

For example, let's say we have an array called scouts and we want to select only a portion of this array based on the array's index.


const scouts = ["levi", "hange", "erwin", "petra", "oruo", "miche"]
scouts.slice(2, 5)
// output: [ 'erwin', 'petra', 'oruo' ]
scouts.slice(1, 3)
// output: [ 'hange', 'erwin' ]

Enter fullscreen mode Exit fullscreen mode

We all know javascript follows a zero-based index, so the first parameter is the index from which we want to start the slice from and the second parameter is the index right after where we want the slice to end. e.g if we want 2 to 4 we use .slice(2, 5) as seen in the first example.

Mapping page number to index

All we need to do is know what the startIndex and lastIndex should be based on the page number. this relationship is quite simple.

Illustration of pagination based on index

As you can tell from the diagram above the last index is simply the current page multiplied by the given content by page, while the first index is the content by page subtracted from the last index.


// assuming we are on page one
const page = 1;
const contentPerPage = 3
const lastIndex = page * contentPerPage // 3
const firstIndex = lastIndex - contentPerPage // 0

scouts.slice(firstIndex, lastIndex)
// scouts.slice(0, 3) => [ 'levi', 'hange', 'erwin' ]

// page 2
// scouts.slice(3, 6) => [ 'petra', 'oruo', 'miche' ]

Enter fullscreen mode Exit fullscreen mode

Wow!, that was easy ๐Ÿ˜ณ.

Custom usePagination hook ๐ŸŽฃ

Now that we've learned the concept behind it, let's implement this in react and create our custom hook to help us automate this process.
This hook takes in an object that takes in the properties contentPerPage which is how many items should be displayed at a time and count which is the total number of items given (Array length). It also returns an object with the following properties.

  • page - current page we are on
  • totalPages - total number of pages generated
  • firstContentIndex - first index for the .slice() method
  • lastContentIndex - last index for the .slice() method
  • nextPage - function to navigate one page foward
  • prevPage - function to navigate one page backward
  • setPage - function to go to a certain page

The type definitions are as follows:


interface UsePaginationProps {
    contentPerPage: number,
    count: number,
}

interface UsePaginationReturn {
    page: number;
    totalPages: number;
    firstContentIndex: number;
    lastContentIndex: number;
    nextPage: () => void;
    prevPage: () => void;
    setPage: (page: number) => void;
}

type UsePagination = (UsePaginationProps) => (UsePaginationReturn);

Enter fullscreen mode Exit fullscreen mode

In your React project create a folder called hooks and create a file called usePagination, this is where our custom hook will reside.

Type the following within it


import { useState } from "react";

const usePagination: UsePagination = ({ contentPerPage, count }) => {
  const [page, setPage] = useState(1);
  // number of pages in total (total items / content on each page)
  const pageCount = Math.ceil(count / contentPerPage);
  // index of last item of current page
  const lastContentIndex = page * contentPerPage;
  // index of first item of current page
  const firstContentIndex = lastContentIndex - contentPerPage;

  // change page based on direction either front or back
  const changePage = (direction: boolean) => {
    setPage((state) => {
      // move forward
      if (direction) {
        // if page is the last page, do nothing
        if (state === pageCount) {
          return state;
        }
        return state + 1;
        // go back
      } else {
        // if page is the first page, do nothing
        if (state === 1) {
          return state;
        }
        return state - 1;
      }
    });
  };

  const setPageSAFE = (num: number) => {
    // if number is greater than number of pages, set to last page
    if (num > pageCount) {
      setPage(pageCount);
      // if number is less than 1, set page to first page
    } else if (num < 1) {
      setPage(1);
    } else {
      setPage(num);
    }
  };

  return {
    totalPages: pageCount,
    nextPage: () => changePage(true),
    prevPage: () => changePage(false),
    setPage: setPageSAFE,
    firstContentIndex,
    lastContentIndex,
    page,
  };
};

export default usePagination;

Enter fullscreen mode Exit fullscreen mode

We are managing the current page value with useState, also notice that pageCount is also equal to the value of the last page. I've made the code above as explanatory as I can.

Implementation โœ๐Ÿพ

We simply import the hook then input the needed properties.

...
  const {
    firstContentIndex,
    lastContentIndex,
    nextPage,
    prevPage,
    page,
    setPage,
    totalPages,
  } = usePagination({
    contentPerPage: 3,
    count: people.length,
  });
...
Enter fullscreen mode Exit fullscreen mode

Then we simply slice our data with the firstContentIndex and lastContentIndex.

...
<div className="items">
  {people
    .slice(firstContentIndex, lastContentIndex)
    .map((el: any) => (
      <div className="item" key={el.uid}></div>
   ))}
</div>
...
Enter fullscreen mode Exit fullscreen mode

Below is a simple functionality to help us generate our buttons, then we add their corresponding onClick handlers.


<div className="pagination">
  <p className="text">
    {page}/{totalPages}
  </p>
  <button onClick={prevPage} className="page">
    &larr;
  </button>
  {/* @ts-ignore */}
  {[...Array(totalPages).keys()].map((el) => (
    <button
      onClick={() => setPage(el + 1)}
      key={el}
      className={`page ${page === el + 1 ? "active" : ""}`}
    >
      {el + 1}
    </button>
  ))}
  <button onClick={nextPage} className="page">
    &rarr;
  </button>
</div>

Enter fullscreen mode Exit fullscreen mode

We are done! As you can see below our usePagination hook works as planned.

showing hook working

Thank you for reading ๐Ÿ™๐Ÿพ, If you have any questions, additions, or subtractions please comment below.

The full source code is linked below ๐Ÿ‘‡๐Ÿ‘‡

GitHub logo damiisdandy / use-pagination

a react usePagination() hook

Top comments (32)

Collapse
 
damiisdandy profile image
damilola jerugba • Edited

Hi Everyone, I've noticed most of you guys are requesting for features like the ability for the repo to handle more than 1000 pages, the usePagination() hook can handle โˆž number of pages. But you guys are should understand that this blog post is simply to show the usePagination() hook and has nothing to do with the pagination buttons or styling. Some of you are requesting that I should add the UI functionality of ... in the middle of the pagination buttons for pages that are as large as a 100 pages. This require for the usePagination() hook to also return a component that contains the pagination buttons.

If you'd like me to implement this, please get the github repo to 50 stars and i'll convert this into a full library that tackles these issues and more.

You are also free to collaborate on this repo so we can build it as a library together ๐Ÿ’™

Thank you ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™

Collapse
 
mbrookes profile image
Matt

Here's a fully functional usePagination hook, with a demo of rendering buttons and an ellipsis: mui.com/components/pagination/#use...

Collapse
 
damiisdandy profile image
damilola jerugba

Thatโ€™s great, but youโ€™ll need install the โ€˜@muiโ€™ library to use it

Thread Thread
 
mbrookes profile image
Matt

Well, sure, but that's a single command (yarn add or npm install). And since all you import is the hook, that's all that will be included in your bundle.

Thread Thread
 
damiisdandy profile image
damilola jerugba

I'll try that, but I'll still challenge myself and create a npm package, it will be my first open source project

Thread Thread
 
mbrookes profile image
Matt

Of course! And perhaps youโ€™ll find a more elegant implementation. Itโ€™s a tricky problem to solve! Good luck! ๐Ÿ‘

Collapse
 
jcubic profile image
Jakub T. Jankiewicz

Do you have a working demo? Does't it work with 1000 pages?

Collapse
 
damiisdandy profile image
damilola jerugba

The number of pages fully depend on the contentPerPage, the API route I used in the code only returns an array of 20 items so the maximum possible number of pages is either 1 or 20, with 3 contentPerPage it generated 7 pages

Here is a working demo => use-pagination.vercel.app/

Collapse
 
jcubic profile image
Jakub T. Jankiewicz • Edited

So it's not fully functional pagination. Just a simple demo.
Here is example how pagination should work (the code is in PHP but it's algorithm that can be rewritten in any language).
gist.github.com/bramus/5d8f2e0269e...

Also note that prev should be disabled when on first page.

Thread Thread
 
damiisdandy profile image
damilola jerugba

It's functional, I was referring to Typescript you were talking about types. There are many ways to implement pagination, this is just my way, and this will work for any language

Thread Thread
 
jcubic profile image
Jakub T. Jankiewicz • Edited

It's functional, I was referring to Typescript you were talking about types.

I don't quite understand what you write this above comment. I just mean that this is not fully implemented pagination, because it's missing fundamental features. So it can't be used in any real projects. You need to implement the thing yourself if you want to have something that is a full pagination. Also this will not work with any language because it's like half of the implementation.

Thread Thread
 
damiisdandy profile image
damilola jerugba • Edited

Sorry I thought I replied to another comment, could you list out these features, so I improve this React hook

Thread Thread
 
damiisdandy profile image
damilola jerugba

You are also free to make contributions to the repo

Thread Thread
 
jcubic profile image
Jakub T. Jankiewicz

Added two issues. I may contribute if the issue is not fixed when I will need pagination since I will need something like this in the near future for my application.

Thread Thread
 
damiisdandy profile image
damilola jerugba

๐Ÿ‘๐Ÿฝ

Collapse
 
roblevintennis profile image
Rob Levin

Thanks for this article @damiisdandy โ€” the illustrations are very helpful and I appreciate the time you spent to share this with the community! Very happy you used buttons too :)

I coded this up for my AgnosticUI library and found my SRP cohesion was best when I made my usePagination.ts hook only concerned with generating the paging links. Then my Pagination.tsx provided the React view and the consumer could simply DI inject the generated pages into the component. The consumer actually keeps track of const [currentPage, setCurrentPage] = useState(1) and listens for a onPageChanged callback resulting in updating the current page. That in turn triggers the useEffect that just asks for the pages to be regenerated with the new current page. It all seems to work quite nicely!

Also, I looked at many many examples on the web and found one somewhere that utilizes currying and utilized that approach as it's extremely efficient and readable:

  const generatePagingPaddedByOne = (current: number, totalPageCount: number) => {
    const center = [current - 1, current, current + 1];
    const filteredCenter: PageArrayItem[] = center.filter((p) => p > 1 && p < totalPageCount);
    const includeLeftDots = current > 3;
    const includeRightDots = current < totalPageCount - 2;
    return getPaddedArray(filteredCenter, includeLeftDots, includeRightDots, totalPageCount);
  };
Enter fullscreen mode Exit fullscreen mode

My API allows you to use 1 or 2 for padding (I've also seen this called one of: "gap" or "offset" or "siblingCount"). Looking at Ant Design and Zen Garden, they both used padding of 1 on both sides but I think 2 is nice to have as an option. The curry approach above is so crystal clear it almost feels like cheating, but, it's also very efficient for large data sets. Thoughts?

I'm actually work-in-progress on this so I don't have a link with live example but I've completed (I think) my React implementation here: pagination component and pagination hook. It gives tabbing for free since it uses buttons (quite similar to how you've done I think; yay for using semantic elements!)

I wonder if this might be an interesting alternative approach for these folks asking about > 1k pages as the curry approach isn't affected by size nor does it require a massive loop. Wish I could take credit but I saw it in a gist "somewhere" and adapted it to my needs. What a challenging programming task to write a good pagination! I'd fail this in a timed interview for sure lol

Collapse
 
damiisdandy profile image
damilola jerugba

Thanks so much, and your idea is solid!

Collapse
 
praveen_g profile image
Prawin G

Hi @damiisdandy just a small doubt,
In changePage function how does state variable work ?
And what's the difference between props data type with and without it ?
say (x : number) || (x)

Btw great code

Collapse
 
damiisdandy profile image
damilola jerugba

the changePage function simply takes in a boolean of either true or false, if true the page state is incremented, and if false the page state is decremented.

there is no functional difference between adding a data type, this is just used so you can't predict the type of value used in the function later and to aid IntelliSense in your IDE. if a data type is not provided typescript will read it as a type of any which is okay.

Also with the useState() hook setPage() can either take in a number or a function that returns a number, which is what I used.

Collapse
 
harryheman profile image
Igor Agapov
setPage: (n: number) => void

const usePagination = ({
  contentPerPage,
  count
}: UsePaginationProps): UsePaginationReturn => { /* ... */ }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
damiisdandy profile image
damilola jerugba

usePaginationReturn is an interface for an object not a function.


type UsePagination = (UsePaginationProps) => (UsePaginationReturn);
const usePagination: UsePagination = ({ contentPerPage, count }) => {  /*...*/ }

Enter fullscreen mode Exit fullscreen mode

The above type is the correct implementation.

Collapse
 
harryheman profile image
Igor Agapov

usePaginationReturn is an interface for an object, returning from a function, so this

type UsePagination = (UsePaginationProps) => (UsePaginationReturn);
const usePagination: UsePagination = ({ contentPerPage, count }) => {  /*...*/ }
Enter fullscreen mode Exit fullscreen mode

is equivalent to this

const usePagination = ({ contentPerPage, count }: UsePaginationProps): UsePaginationReturn => { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Just -1 type

Collapse
 
theharshrastogi profile image
Harsh Rastogi

Nice

Collapse
 
damiisdandy profile image
damilola jerugba

Thanks, glad you liked it

Collapse
 
rag_marcus_321 profile image
Raghav Gupta

Awesome

Collapse
 
damiisdandy profile image
damilola jerugba

Thanks ๐Ÿ‘๐Ÿฝ

Collapse
 
shaalanmarwan profile image
Shaalan Marwan

Very clear , thanks

Collapse
 
damiisdandy profile image
damilola jerugba

You are welcome โค๏ธ

Collapse
 
damiisdandy profile image
damilola jerugba

Thank you โšก

Collapse
 
tzii profile image
Vu

it doesn't work when you have many pages like more than 100

Collapse
 
damiisdandy profile image
damilola jerugba

Can you share a piece of the code you wrote, 100 pages depends on the content per page. It should work for any number.