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)
Do you have it in Github as source code?
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. 😉