≡ Menu

How to Build a Pagination Feature In React 🚀

Managing large datasets in your React application can slow down performance and overwhelm your users.

The solution?

Pagination!

Instead of rendering a massive list all at once, you show a manageable chunk, improving both speed and user experience.

In this tutorial, we’ll walk through a complete, clean React component that fetches data using the native Fetch API (no bulky third-party libraries needed!) and implements robust pagination logic. We’ll use the JSONPlaceholder API for our dummy data.


1. Setting Up the State and Imports

We’ll start with the necessary imports from React and define the initial state for our data, pagination settings, and current page.

import React, { useState, useEffect } from 'react';

const App = () => {
  // todos: Holds the full array of data fetched from the API
  const [todos, setTodos] = useState([]);
  
  // todosPerPage: Determines how many items show on one page
  const [todosPerPage] = useState(10); 
  
  // currentPage: Tracks which page the user is currently viewing
  const [currentPage, setCurrentPage] = useState(1);

  // ... rest of the component

2. Fetching Data with useEffect and Fetch API

We use the useEffect hook to fetch the data only once when the component mounts. Notice how we’re using the native fetch API, which is a modern, built-in alternative to older libraries like Axios.

💡 Fetch API Tip: Unlike Axios, the native fetch returns a raw response object, so you must explicitly call .json() on the response to parse the body into a JavaScript object.

  // Fetch data only once on mount
  useEffect(() => {
    // ⬇️ Using the native Fetch API 
    fetch("https://jsonplaceholder.typicode.com/todos")
      .then((res) => res.json()) // 1. Convert the raw response to JSON
      .then((data) => setTodos(data)) // 2. Use the JSON data to update state
      .catch((error) => console.error("Error fetching todos:", error)); // 3. Error handling is crucial!
  }, []); // The empty array dependency ensures it runs only once.

3. Implementing the Core Pagination Logic

The magic of pagination happens here. We need three key pieces of information to determine what to show:

  1. Total number of pages: Found by dividing the total items by items per page and rounding up using Math.ceil().
  2. Indices for slicing: We calculate the starting (indexOfFirstTodo) and ending (indexOfLastTodo) indices for the array slice based on the current page.
  3. The visible data: We use the array’s .slice() method to extract only the items for the current page.
// --- Pagination Logic ---
  // Calculate total pages needed
  const numOfTotalPages = Math.ceil(todos.length / todosPerPage);
  
  // Create an array of page numbers: [1, 2, 3, ..., numOfTotalPages]
  const pages = [...Array(numOfTotalPages + 1).keys()].slice(1);

  // Calculate the index range for the current page's slice
  const indexOfLastTodo = currentPage * todosPerPage;
  const indexOfFirstTodo = indexOfLastTodo - todosPerPage;

  // Slice the full todos array to get only the items for the current view
  const visibleTodos = todos.slice(indexOfFirstTodo, indexOfLastTodo);

  // ... rest of the component

4. Navigation Handlers

We’ll create simple functions to handle clicking the “Prev” and “Next” buttons, ensuring the page number doesn’t go below 1 or above the total number of pages.

  // --- Handlers ---
  const prevPageHandler = () => {
    // Only allow navigating back if not on the first page
    if (currentPage !== 1) setCurrentPage(currentPage - 1);
  };

  const nextPageHandler = () => {
    // Only allow navigating forward if not on the last page
    if (currentPage !== numOfTotalPages) setCurrentPage(currentPage + 1);
  };

5. Rendering the UI and Controls

Finally, we render the data and the pagination controls. We use the visibleTodos array for the main list and the pages array for generating the clickable page numbers.

  return (
    <div>
      <h2>Todo List (Page {currentPage})</h2>
      
      {/* Display a loading message while todos are empty */}
      {todos.length === 0 ? (
        <p>Loading todos...</p>
      ) : (
        /* MAP OVER THE VISIBLE TODOS ONLY */
        visibleTodos.map((todo) =>
          <p key={todo.id}>
            **{todo.id}**: {todo.title} 
            {/* Optional: Show status based on 'completed' property */}
          </p>
        )
      )}

      <hr />

      {/* PAGINATION CONTROLS */}
      <span 
        onClick={prevPageHandler}
        // Disable 'Prev' button visually and functionally on page 1
        style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
      >
        Prev
      </span>
      
      <p>
        {pages.map((page) => (
          <span
            key={page}
            onClick={() => setCurrentPage(page)}
            className={`${currentPage === page ? "active" : ""}`} 
            style={{ 
              margin: '0 5px', 
              fontWeight: currentPage === page ? 'bold' : 'normal',
              cursor: 'pointer'
            }}
          >
            {`${page} | `}
          </span>
        ))}
      </p>

      <span 
        onClick={nextPageHandler}
        // Disable 'Next' button visually and functionally on the last page
        style={{ cursor: currentPage === numOfTotalPages ? 'not-allowed' : 'pointer' }}
      >
        Next
      </span>
      
      {/* A simple style for the active class for demonstration */}
      <style>{`
        .active {
          color: blue;
          text-decoration: underline;
        }
      `}</style>
    </div>
  );
};

export default App;

Full Code Summary

Here is the complete code for your App.js component:

import React, { useState, useEffect } from 'react';

const App = () => {
  const [todos, setTodos] = useState([]);
  const [todosPerPage] = useState(10); 
  const [currentPage, setCurrentPage] = useState(1);

  // Fetch data only once on mount
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/todos")
      .then((res) => res.json()) 
      .then((data) => setTodos(data)) 
      .catch((error) => console.error("Error fetching todos:", error)); 
  }, []);

  // --- Pagination Logic ---
  const numOfTotalPages = Math.ceil(todos.length / todosPerPage);
  const pages = [...Array(numOfTotalPages + 1).keys()].slice(1);

  const indexOfLastTodo = currentPage * todosPerPage;
  const indexOfFirstTodo = indexOfLastTodo - todosPerPage;

  const visibleTodos = todos.slice(indexOfFirstTodo, indexOfLastTodo);

  // --- Handlers ---
  const prevPageHandler = () => {
    if (currentPage !== 1) setCurrentPage(currentPage - 1);
  };

  const nextPageHandler = () => {
    if (currentPage !== numOfTotalPages) setCurrentPage(currentPage + 1);
  };

  return (
    <div>
      <h2>Todo List (Page {currentPage})</h2>
      
      {/* Display a loading message while todos are empty */}
      {todos.length === 0 ? (
        <p>Loading todos...</p>
      ) : (
        /* MAP OVER THE VISIBLE TODOS ONLY */
        visibleTodos.map((todo) =>
          <p key={todo.id}>**{todo.id}**: {todo.title}</p>
        )
      )}

      <hr />

      {/* PAGINATION CONTROLS */}
      <span 
        onClick={prevPageHandler}
        style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
      >
        Prev
      </span>
      
      <p>
        {pages.map((page) => (
          <span
            key={page}
            onClick={() => setCurrentPage(page)}
            className={`${currentPage === page ? "active" : ""}`} 
            style={{ 
              margin: '0 5px', 
              fontWeight: currentPage === page ? 'bold' : 'normal',
              cursor: 'pointer'
            }}
          >
            {`${page} | `}
          </span>
        ))}
      </p>

      <span 
        onClick={nextPageHandler}
        style={{ cursor: currentPage === numOfTotalPages ? 'not-allowed' : 'pointer' }}
      >
        Next
      </span>
      
      {/* A simple style for the active class for demonstration */}
      <style>{`
        .active {
          color: blue;
          text-decoration: underline;
        }
      `}</style>
    </div>
  );
};

export default App;

This setup provides a clean, efficient, and library-free way to handle both data fetching and client-side pagination in your React applications! Happy coding! 💻

{ 0 comments… add one }

Leave a Comment