MikeGajdos
Posted on November 15, 2021
I was asked by a friend of mine to implement pagination on his blog . He wanted the previous and next buttons as well as the first and the last page buttons to be always shown at the bottom of the page and have an active page button showing with one "sibling " on each side in case the visitor is navigating through the pagination button group.
This blog post is kinda (!!) my little attempt at implementing it.
So these are the possible scenarios scenarios...
I won't go through why anyone would choose pagination over an infinite scroll or what are the benefits of each approach. I wrote a little blog post earlier about the basic concept of pagination in React check it out here.
TL;DR : In my opinion you should choose pagination when you know the size of your dataset in advance and the dataset does not change frequently. When it comes to pagination in React it is all about "reacting" ( pun intended) to a change of a currentPage state variable and based on that change slicing the data accordingly with desired constant variable contentPerPage of how many items we would like to show on our UI.
To test my thought process I created a little test demo with create-react-app
and json
placeholder API ( a superb API for mockups).
This is the final result ๐
My thinking went something like this: My App will receive all the posts ( JSON Placeholder API has an endpoint with 100 blog posts) and then my Pagination component will get that data as a prop from a top-level App Component, along with some constant variables that will not change, and it will manage all the magic of handling navigating to the next page etc. I know it goes against React's lifting the state up approach but I wanted take the approach where the goPagination will manage its own state.t
So my Pagination component will receive these props:
- data : all the posts from App component after successful fetch request
- RenderComponent: essentially each individual post, be it a div or list item
- contentPerPage: constant which is essential with any pagination. It will determine how many posts we would like to display in our UI. It is important for slicing and the total size of the dataset.
- siblingCount: a variable that indicates desired amount of buttons next to the active button when navigating through the pagination buttons
- buttonConst: this prop is essential for determining that we always want 1st page, last page, and active button to be shown so we will set this to be 3
Let's work through the code. App.js
looks like this. It follows the outline we described above. It fetches the data using useEffect
hooks and passes it down to our custom Pagination Component with few state constants.
// App.js
import React, { useState, useEffect } from "react";
import Pagination from "./components/Pagination";
import Post from "./components/Post";
const url = "https://jsonplaceholder.typicode.com/posts";
function App() {
const [posts, setPosts] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
fetch(url)
.then((response) => {
if (response.ok) return response.json();
throw new Error("something went wrong while requesting the data");
})
.then((posts) => {
setPosts(posts);
})
.catch((error) => setError(error.message));
}, []);
if (error) return <h1>{error}</h1>;
return (
<div>
{posts.length > 0 ? (
<>
<Pagination
data={posts}
RenderComponent={Post}
title="Posts"
buttonConst={3}
contentPerPage={5}
siblingCount={1}
/>
</>
) : (
<h1>No Posts to display</h1>
)}
</div>
);
}
export default App;
Our Pagination Component :
It will hold 3 values in its state :
- totalPageCount : essentially total amount page buttons available . It will be derived from the props received from the parent (APP component)
- currentPage: default value set to 1. It will our indicator in our pagination implementation.
- paginationRange : which basically array of button numbers . It will be derived using our custom
usePaginationRange
hook. It will take props and essentially spits out an array of value based on currentPage .Please see the code for it below. I highlighted the comments with a little explanation.
I conditionally applied a few classes in CSS like "disabled" and " active" based on currentPage for better UX
//Pagination.js
import React, { useState, useEffect } from "react";
import { usePaginationRange, DOTS } from "../hooks/usePaginationRange";
const Pagination = ({
data,
RenderComponent,
title,
buttonConst,
contentPerPage,
siblingCount,
}) => {
const [totalPageCount] = useState(Math.ceil(data.length / contentPerPage));
const [currentPage, setCurrentPage] = useState(1);
const paginationRange = usePaginationRange({
totalPageCount,
contentPerPage,
buttonConst,
siblingCount,
currentPage,
});
/* ๐ little UX tweak when user clicks on any button we scoll to top of the page */
useEffect(() => {
window.scrollTo({
behavior: "smooth",
top: "0px",
});
}, [currentPage]);
function goToNextPage() {
setCurrentPage((page) => page + 1);
}
function gotToPreviousPage() {
setCurrentPage((page) => page - 1);
}
function changePage(event) {
const pageNumber = Number(event.target.textContent);
setCurrentPage(pageNumber);
}
const getPaginatedData = () => {
const startIndex = currentPage * contentPerPage - contentPerPage;
const endIndex = startIndex + contentPerPage;
return data.slice(startIndex, endIndex);
};
return (
<div>
<h1>{title}</h1>
{/* show the post 10 post at a time*/}
<div className="dataContainer">
{getPaginatedData().map((dataItem, index) => (
<RenderComponent key={index} data={dataItem} />
))}
</div>
{/* show the pagiantion
it consists of next and previous buttons
along with page numbers, in our case, 5 page
numbers at a time */}
<div className="pagination">
{/* previous button */}
<button
onClick={gotToPreviousPage}
className={` prev ${currentPage === 1 ? "disabled" : ""}`}
>
previous
</button>
{/* show paginated button group */}
{paginationRange.map((item, index) => {
if (item === DOTS) {
return (
<button key={index} className={`paginationItem`}>
…
</button>
);
}
return (
<button
key={index}
onClick={changePage}
className={`paginationItem ${
currentPage === item ? "active" : null
}`}
>
<span>{item}</span>
</button>
);
})}
{/* next button */}
<button
onClick={goToNextPage}
className={`next ${currentPage === totalPageCount ? "disabled" : ""}`}
>
next
</button>
</div>
</div>
);
};
export default Pagination;
Our custom usePaginateRange
hook :
Here is what the usePaginateRange hook looks like. Please read the inline comments for further explanation
//usePaginationRange.js
import { useMemo } from "react";
export const DOTS = "...";
// our range generator function
const range = (start, end) => {
let length = end - start + 1;
return Array.from({ length }, (_, index) => index + start);
};
export const usePaginationRange = ({
totalPageCount,
dataLimit,
buttonConst,
siblingCount,
currentPage,
}) => {
const paginationRange = useMemo(() => {
// Pages count is determined as siblingCount + buttonConst(firstPage + lastPage + currentPage) + 2*DOTS
const totalPageNumbers = buttonConst + 2 + siblingCount;
/*
If the number of pages is less than the page numbers we want to show in our
paginationComponent, we return the range [1..totalPageCount]
*/
if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount);
}
/*
Calculate left and right sibling index and make sure they are within range 1 and totalPageCount
*/
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
const rightSiblingIndex = Math.min(
currentPage + siblingCount,
totalPageCount
);
/*
We do not want to show dots if there is only one position left
after/before the left/right page count as that would lead to a change if our Pagination
component size which we do not want
*/
const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex <= totalPageCount - 2;
const firstPageIndex = 1;
const lastPageIndex = totalPageCount;
/*
No left dots to show, but rights dots to be shown
*/
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount);
return [...leftRange, DOTS, totalPageCount];
}
/*
No right dots to show, but left dots to be shown
*/
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount;
let rightRange = range(
totalPageCount - rightItemCount + 1,
totalPageCount
);
return [firstPageIndex, DOTS, ...rightRange];
}
/*
Both left and right dots to be shown
*/
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex);
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex];
}
}, [totalPageCount, siblingCount, currentPage, buttonConst]);
return paginationRange;
};
Ideally, the page should scroll to the top whenever we change the page. This can easily be implemented by using the useEffect hook which executes whenever the current state changes . It makes for better UX. I added this piece of code into Pagination Component to implement this.
useEffect(() => {
window.scrollTo({
behavior: "smooth",
top: "0px",
});
}, [currentPage]);
I hope this was helpful to someone.The code needs a bit of work to improve. I would greatly appreciate any feedback.
For a full code please check out my GitHub repo here.
Resources:
https://academind.com/tutorials/reactjs-pagination
https://www.freecodecamp.org/news/build-a-custom-pagination-component-in-react/
Posted on November 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 30, 2024