Pagination With React Enter a brief summary for this post

Pagination with React

I had been using React Query a few times. It proves to be useful and lets you get on with things but at a price. That price being it may be over wielding and bloated for most projects.

That's always an issue with me. Another issue of course are new releases breaking existing code and making it problematic on new projects. You've to stop and go figure out the changes and adjust accordingly.

With version 5 I decided enough was enough and throw together my own pagination solution.

Using Axios for a Service Layer Making API Calls

I use a service layer to access the API which can be found below. Put this service to ./src/Services/PostService.jsx.

import apiInstance from '../Utils/ApiInstance';

async function ping() {
    return await apiInstance.get('/sanctum/csrf-cookie');
};

async function paginate(page) { 
    return await apiInstance.get('/api/v1/posts/paginate?page=' + page);
};

async function getOne(id) {
    return await apiInstance.get('/api/v1/posts/' + id);
};

async function create(data) {
    return await apiInstance.post('/api/v1/posts', data);
};

async function update(id, data) {
    return await apiInstance.put('/api/v1/posts/' + id, data);
};

async function remove(id) {
    return await apiInstance.delete('/api/v1/posts/' + id);
};

const PostService = {
    ping,
    paginate,
    getOne,
    create,
    update,
    remove
};

export default PostService;

The apiInstance implementation is stored under the utility's directory (./src/Utils/ApiInstance.jsx).

import axios from 'axios';

const apiInstance = axios.create({
	baseURL: 'http://localhost:8080',
  	timeout: 0,
    withCredentials: true,
	withXSRFToken: true, 
  	xsrfCookieName: "XSRF-TOKEN",	
  	xsrfHeaderName: "X-XSRF-TOKEN",	

  	responseType: 'json',
  	responseEncoding: 'utf8',
  	headers: {
  		'Content-Type': 'application/json; charset=UTF-8',
		'Accept': 'application/json; charset=UTF-8',
		'Referrer-Policy': 'strict-origin-when-cross-origin',
		'Cache-Control': 'no-cache',
		'X-Requested-With': 'XMLHttpRequest',
  	},
});

apiInstance.interceptors.request.use(config => {

	return config;
}, error => {

	return Promise.reject(error);
});

apiInstance.interceptors.response.use(response => {

	return response;
}, error => {

	return Promise.reject(error);
});

export default apiInstance;

Starting with the hook ./src/Hooks/usePagination.jsx is where the more interesting things happen. Take a look below. I feel there is more work to be done with it and some point in the future there may be a refactoring.

import { useState } from "react";

function usePagination(totalPosts, itemsLimit, siblingCount) {
    const [pageNumber, setPageNumber] = useState(1);
    const pageCount = Math.ceil(totalPosts / itemsLimit);

    const startIndex = (pageNumber - 1) * itemsLimit;
    const endIndex = Math.min(startIndex + itemsLimit - 1, totalPosts - 1);

    const leftSiblingIndex = Math.max(pageNumber - siblingCount, 1);
    const rightSiblingIndex = Math.min(pageNumber + siblingCount, totalPosts);

    const shouldShowLeftDots = leftSiblingIndex > 2;
    const shouldShowRightDots = rightSiblingIndex < pageCount - 2;

    const firstPageIndex = 1;
    const lastPageIndex = pageCount;

    const changePage = (page) => {
        setPageNumber(page);
    };

    const nextPage = () => {
        setPageNumber(Math.min(pageNumber + 1, pageCount));
    };

    const previousPage = () => {
        if(Math.max(pageNumber - 1, 0) >= 1) {
            setPageNumber(Math.max(pageNumber - 1, 0));
        } else {
            setPageNumber(1);
        }
    };

    const startPage = () => {
        changePage(1);
    };

    const endPage = () => {
        changePage(pageCount);
    };

    return {
        pageNumber,
        pageCount,
        changePage,
        nextPage,
        previousPage,
        startPage,
        endPage,
        startIndex,
        endIndex,
        leftSiblingIndex,
        rightSiblingIndex,
        shouldShowLeftDots,
        shouldShowRightDots
    };
}

export default usePagination;

With this example of pagination with React the beginnings take place in the ./src/App.jsx file, seen below. Not including the hook there are a few other components imported.

Those are <Pagination />, <PaginationButtons />, <PaginationButton /> and <PaginationButtonDots /> following below.

import { useEffect, useState } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.css';

import apiInstance from './Utils/ApiInstance';
import PostService from './Services/PostService';

import Pagination from './Components/Pagination';

function App() {
    const itemsLimit = 12;                              // number of rows per page
    const [totalPosts, setTotalPosts] = useState(0);    // total rows altogether
    const [posts, setPosts] = useState([]);             
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);
        PostService.paginate(1).then(response => {
            setTotalPosts(response.data?.data?.total);
            setPosts(response.data?.data?.data);

            setLoading(false);
        }).catch(error => {

        })
    }, []);

    return (
        <>
        <div className="container">
            <div className="row">
                <div className="col-lg-12">

            
                
                    <Pagination
                        totalPosts={totalPosts}         // total number of rows altogether
                        posts={posts}
                        itemsLimit={itemsLimit}         // number of rows per page displayed
                        service={PostService}           // access the API from service layer
                        setPosts={setPosts}             // store new rows of data
                        siblingCount={1} 
                    />
                </div>
            </div>
            <div className="row">
                <div className="col-lg-12">

                    <ul>
                    {
                        posts.map((post, i) => (
                            <li key={i}>{post.name}</li>
                        ))
                    }
                    </ul>
                
                </div>
            </div>
        </div></>
    );
}

export default App;

import { useEffect, useState } from 'react';
import usePagination from '../Hooks/usePagination';

import PaginationButtons from './PaginationButtons';

const Pagination = (props) => { 
    const { 
        pageNumber,             // the current paginated page
        pageCount,              // number of paginated pages, based on total divided by rows displayed
        changePage, 
        nextPage, 
        previousPage,
        startPage,
        endPage,
        startIndex,
        endIndex,
        leftSiblingIndex,
        rightSiblingIndex,
        shouldShowLeftDots,
        shouldShowRightDots
    } = usePagination(props.totalPosts, props.itemsLimit, props.siblingCount);

    useEffect(() => {
        props.service.paginate(pageNumber).then(response => {
            props.setPosts(response.data?.data?.data);
        }).catch(error => {
            console.log(error);
        })
    }, [pageNumber]);

    return (
        <div>
            <button className="bg-info mx-2 p-2" onClick={() => startPage()}>Start</button>
            <button className="bg-info mx-2 p-2" onClick={previousPage}>Prev</button>
            
            <PaginationButtons 
                pageNumber={pageNumber} 
                pageCount={pageCount}
                changePage={changePage}
            />

            <button className="bg-info mx-2 p-2" onClick={nextPage}>Next</button>
            <button className="bg-info mx-2 p-2" onClick={() => endPage()}>End</button>
        </div>
    );
};

export default Pagination;

import PaginationButton from './PaginationButton';
import PaginationButtonDots from './PaginationButtonDots';

function PaginationButtons({pageNumber, pageCount, changePage}) {
	
	const content = () => {
	    let content = [];
	    for(let i = 1; i < pageCount +1; i++) {
	    	if(i >= pageNumber - 3 && i <= pageNumber + 3) {
	    		content.push(<PaginationButton number={i} pageNumber={pageNumber} handler={changePage} />);
	    	} 
	    }

	    if(pageNumber <= 3) { 
	    	content.unshift(<PaginationButtonDots number={pageNumber} />);
	    }

	    if(pageNumber > pageCount - 3) { 
	    	content.push(<PaginationButtonDots number={pageNumber} />);
	    }

	    return content;
	};
	
	return (
	    <>
	    	<ul className='buttons'>{content()}</ul>
	    </>
	);
	
}

export default PaginationButtons;

function PaginationButton({number, pageNumber, handler}) {

	return (
	    <>
	    	<li key={number}>
	    	{number === pageNumber && <button className="bg-danger mx-2 p-2">{number}</button>}
	    	{number !== pageNumber && <button className="bg-info mx-2 p-2" onClick={() => handler(number)}>{number}</button>}
			</li>
		</>
	);

}

export default PaginationButton;

function PaginationButtonDots({number}) {

	return (
	    <>
	    	<li key={number}>
	    		<button className="bg-info mx-2 p-2"> ... </button>
			</li>
		</>
	);
}

export default PaginationButtonDots;

Rudimentary perhaps but the workings are there for you to take it further.