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
fetchreturns 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:
- Total number of pages: Found by dividing the total items by items per page and rounding up using
Math.ceil(). - Indices for slicing: We calculate the starting (
indexOfFirstTodo) and ending (indexOfLastTodo) indices for the array slice based on the current page. - 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! 💻



