Building a Custom Pagination Component in React: A Comprehensive Guide

Pagination is a crucial feature in many web applications, especially when dealing with large datasets or content-heavy pages. It allows users to navigate through data in manageable chunks, improving the user experience and performance of the application. In this blog post, we‘ll dive deep into building a custom pagination component in React, covering everything from the basics to advanced features and best practices.

Understanding Pagination

Before we start building our pagination component, let‘s take a moment to understand what pagination is and why it‘s important. Pagination is the process of dividing content into discrete pages, with each page displaying a subset of the total data. This is particularly useful when dealing with large amounts of data that can‘t be displayed on a single page without overwhelming the user or impacting performance.

There are different types of pagination, such as numbered pages, infinite scroll, and load more buttons. In this blog post, we‘ll focus on numbered pages, which is the most common type of pagination. Numbered pages display a set of page numbers that users can click on to navigate to a specific page.

Setting up the Project

To get started, let‘s create a new React project using Create React App. Open your terminal and run the following command:

npx create-react-app react-pagination-demo
cd react-pagination-demo

Next, let‘s install the necessary dependencies. For this project, we‘ll be using the classnames library to conditionally apply CSS classes. Run the following command to install it:

npm install classnames

Now, let‘s create the basic folder structure for our pagination component. Inside the src folder, create a new folder called components and inside it, create a file named Pagination.js. This is where we‘ll build our pagination component.

Building the Pagination Component

Let‘s start by designing the interface and props for our pagination component. The component will accept the following props:

  • currentPage: The current active page number.
  • totalItems: The total number of items to be paginated.
  • pageSize: The number of items to be displayed per page.
  • onPageChange: A callback function to be invoked when the page changes.
  • className (optional): Additional CSS class name for the component.

Here‘s the basic structure of our Pagination component:

import React from ‘react‘;
import classNames from ‘classnames‘;

const Pagination = ({ currentPage, totalItems, pageSize, onPageChange, className }) => {
  // Logic for generating page numbers goes here

  return (
    <div className={classNames(‘pagination‘, className)}>
      {/* Render page numbers */}
    </div>
  );
};

export default Pagination;

Now, let‘s implement the core logic for generating page numbers. We‘ll create a function called getPageNumbers that takes in the current page, total items, and page size, and returns an array of page numbers to be rendered.

const getPageNumbers = (currentPage, totalItems, pageSize) => {
  const totalPages = Math.ceil(totalItems / pageSize);
  const pageNumbers = [];

  // Logic for generating page numbers goes here

  return pageNumbers;
};

Inside the getPageNumbers function, we‘ll handle different scenarios based on the total number of pages and the current page. Here‘s the complete implementation:

const getPageNumbers = (currentPage, totalItems, pageSize) => {
  const totalPages = Math.ceil(totalItems / pageSize);
  const pageNumbers = [];

  if (totalPages <= 5) {
    // If there are 5 or fewer pages, display all page numbers
    for (let i = 1; i <= totalPages; i++) {
      pageNumbers.push(i);
    }
  } else {
    // If there are more than 5 pages, display first, last, and surrounding pages
    const firstPage = 1;
    const lastPage = totalPages;

    if (currentPage <= 3) {
      // If current page is close to the beginning
      for (let i = 1; i <= 5; i++) {
        pageNumbers.push(i);
      }
      pageNumbers.push(‘...‘);
      pageNumbers.push(lastPage);
    } else if (currentPage >= totalPages - 2) {
      // If current page is close to the end
      pageNumbers.push(firstPage);
      pageNumbers.push(‘...‘);
      for (let i = totalPages - 4; i <= totalPages; i++) {
        pageNumbers.push(i);
      }
    } else {
      // If current page is in the middle
      pageNumbers.push(firstPage);
      pageNumbers.push(‘...‘);
      for (let i = currentPage - 1; i <= currentPage + 1; i++) {
        pageNumbers.push(i);
      }
      pageNumbers.push(‘...‘);
      pageNumbers.push(lastPage);
    }
  }

  return pageNumbers;
};

In the above code, we handle three scenarios:

  1. If there are 5 or fewer pages, we display all page numbers.
  2. If the current page is close to the beginning (less than or equal to 3), we display the first 5 pages, followed by dots and the last page.
  3. If the current page is close to the end (greater than or equal to totalPages - 2), we display the first page, followed by dots and the last 5 pages.
  4. If the current page is in the middle, we display the first page, dots, the current page with its surrounding pages, dots, and the last page.

Now that we have the logic for generating page numbers, let‘s render them in our Pagination component:

const Pagination = ({ currentPage, totalItems, pageSize, onPageChange, className }) => {
  const pageNumbers = getPageNumbers(currentPage, totalItems, pageSize);

  return (
    <div className={classNames(‘pagination‘, className)}>
      {pageNumbers.map((number, index) => (
        <button
          key={index}
          className={classNames(‘pagination__item‘, {
            ‘pagination__item--active‘: number === currentPage,
            ‘pagination__item--dots‘: number === ‘...‘,
          })}
          onClick={() => onPageChange(number)}
          disabled={number === ‘...‘}
        >
          {number}
        </button>
      ))}
    </div>
  );
};

In the above code, we map over the pageNumbers array and render each number as a button. We apply conditional CSS classes based on whether the number is the current page or dots. We also disable the button if it represents dots.

Implementing the Pagination Hook

To make our pagination component more reusable and encapsulate the pagination logic, let‘s create a custom hook called usePagination. This hook will accept the current page, total items, and page size as parameters and return the necessary data for rendering the pagination component.

Create a new file called usePagination.js inside the src folder and add the following code:

import { useMemo } from ‘react‘;

const usePagination = (currentPage, totalItems, pageSize) => {
  const pageNumbers = useMemo(() => {
    const totalPages = Math.ceil(totalItems / pageSize);
    const numbers = [];

    // Logic for generating page numbers goes here

    return numbers;
  }, [currentPage, totalItems, pageSize]);

  return {
    currentPage,
    totalPages: Math.ceil(totalItems / pageSize),
    pageNumbers,
  };
};

export default usePagination;

In the usePagination hook, we use the useMemo hook to memoize the generated page numbers. This ensures that the page numbers are only recalculated when the current page, total items, or page size changes.

You can copy the logic for generating page numbers from the getPageNumbers function we implemented earlier and use it inside the useMemo callback.

Now, let‘s update our Pagination component to use the usePagination hook:

import React from ‘react‘;
import classNames from ‘classnames‘;
import usePagination from ‘../hooks/usePagination‘;

const Pagination = ({ currentPage, totalItems, pageSize, onPageChange, className }) => {
  const { pageNumbers } = usePagination(currentPage, totalItems, pageSize);

  return (
    <div className={classNames(‘pagination‘, className)}>
      {pageNumbers.map((number, index) => (
        <button
          key={index}
          className={classNames(‘pagination__item‘, {
            ‘pagination__item--active‘: number === currentPage,
            ‘pagination__item--dots‘: number === ‘...‘,
          })}
          onClick={() => onPageChange(number)}
          disabled={number === ‘...‘}
        >
          {number}
        </button>
      ))}
    </div>
  );
};

export default Pagination;

Integrating the Pagination Component

Now that we have our pagination component and hook ready, let‘s see how we can integrate it into a real-world scenario. Suppose we have an API that returns a list of products, and we want to display these products in a paginated fashion.

First, let‘s create a new component called ProductList and fetch the data from the API:

import React, { useState, useEffect } from ‘react‘;
import Pagination from ‘./Pagination‘;

const ProductList = () => {
  const [products, setProducts] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [totalItems, setTotalItems] = useState(0);
  const pageSize = 10;

  useEffect(() => {
    fetchProducts();
  }, [currentPage]);

  const fetchProducts = async () => {
    try {
      const response = await fetch(`/api/products?page=${currentPage}&pageSize=${pageSize}`);
      const data = await response.json();
      setProducts(data.products);
      setTotalItems(data.totalItems);
    } catch (error) {
      console.error(‘Error fetching products:‘, error);
    }
  };

  const handlePageChange = (pageNumber) => {
    setCurrentPage(pageNumber);
  };

  return (
    <div>

      {/* Render products */}
      <Pagination
        currentPage={currentPage}
        totalItems={totalItems}
        pageSize={pageSize}
        onPageChange={handlePageChange}
      />
    </div>
  );
};

export default ProductList;

In the ProductList component, we use the useState hook to manage the state for products, current page, and total items. We also define a pageSize constant to determine the number of products to display per page.

Inside the useEffect hook, we call the fetchProducts function whenever the current page changes. The fetchProducts function makes an API request to fetch the products for the current page and updates the state accordingly.

We pass the necessary props to the Pagination component, including the current page, total items, page size, and the handlePageChange callback function. The handlePageChange function is called whenever the user clicks on a page number, and it updates the current page state.

Advanced Features and Optimizations

To enhance the functionality and usability of our pagination component, we can consider adding some advanced features and optimizations:

  1. Page Size Selection: Allow users to choose the number of items displayed per page by providing a dropdown or input field for page size selection. Update the pageSize state and re-fetch the data whenever the page size changes.

  2. Lazy Loading: Instead of fetching all the data at once, implement lazy loading to fetch data as the user navigates through the pages. This improves performance and reduces the initial load time.

  3. Accessibility: Ensure that the pagination component is accessible by providing proper ARIA attributes and keyboard navigation support. Use semantic HTML elements and follow accessibility best practices.

  4. Styling: Customize the styling of the pagination component to match your application‘s design. Use CSS modules or styled-components to encapsulate the styles and avoid class name conflicts.

Best Practices and Tips

When implementing pagination in your React application, consider the following best practices and tips:

  1. Error Handling: Implement proper error handling and display informative messages to the user if there are any issues with fetching data or updating the page.

  2. Loading State: Show a loading indicator or placeholder content while the data is being fetched to provide visual feedback to the user.

  3. Caching: Implement caching mechanisms to store fetched data and avoid unnecessary API requests when navigating back to previously visited pages.

  4. Server-Side Pagination: If you have a large dataset, consider implementing server-side pagination to reduce the amount of data transferred to the client and improve performance.

  5. Responsive Design: Ensure that the pagination component is responsive and adapts well to different screen sizes and devices.

Conclusion

In this blog post, we explored the process of building a custom pagination component in React. We covered the basics of pagination, setting up the project, implementing the pagination component and hook, integrating it into a real-world scenario, and discussing advanced features and best practices.

Pagination is a vital feature in many web applications, and building a reusable and customizable pagination component can greatly enhance the user experience and performance of your application.

Remember to consider the specific requirements of your application and adapt the pagination component accordingly. Experiment with different styles, layouts, and features to find the best fit for your project.

I hope this comprehensive guide has provided you with a solid foundation for implementing pagination in your React applications. Feel free to explore further and customize the component to suit your needs.

Happy coding!

Similar Posts