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. 😉