Rendering large datasets directly into tables often leads to cluttered UIs and poor performance. Whether you’re building a user list, an order summary, or an admin dashboard, breaking down the dataset into manageable pages improves both usability and responsiveness. In React, implementing pagination can be done manually using hooks, or with the help of libraries that abstract some of the repetitive logic.
This blog walks through both options—custom pagination logic using useState
and useEffect
, and a library-driven approach using react-paginate
. Each method will help you render dynamic tables cleanly, and adapt easily whether you’re working with local data or integrating a paginated API.
What We’re Solving?
We need to display a long list of data (e.g. users) in a table with a fixed number of rows per page. Using React functional components with hooks, we’ll fetch data from an API and implement pagination on the client side. We show two methods: a custom hook-based pagination (slicing the data array) and using the react-paginate
library. We will also outline how to adapt these for server-side pagination (where the API returns only one page at a time).
Approach 1: Custom Pagination using React Hooks
Use useState
and useEffect
to fetch the full list once and then slice it per page. For example:
function UsersTable() {
const [users, setUsers] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;
// Fetch data on mount
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
// Calculate slice indices for current page
const indexOfLast = currentPage * itemsPerPage;
const indexOfFirst = indexOfLast - itemsPerPage;
const currentUsers = users.slice(indexOfFirst, indexOfLast); // only rows for current page:contentReference[oaicite:2]{index=2}
return (
<>
<table>
<thead>
<tr><th>ID</th><th>Name</th><th>Email</th></tr>
</thead>
<tbody>
{currentUsers.map(user => (
<tr key={user.id}>
<td>{user.id}</td><td>{user.name}</td><td>{user.email}</td>
</tr>
))}
</tbody>
</table>
<div className="pagination">
{Array.from({ length: Math.ceil(users.length / itemsPerPage) }, (_, i) => (
<button key={i} onClick={() => setCurrentPage(i + 1)}>
{i + 1}
</button>
))}
</div>
</>
);
}
JavaScriptThis component uses useState
for currentPage
and a constant itemsPerPage
(5) as recommended. We fetch users
once with useEffect
. We compute indexOfFirst
and indexOfLast
and use users.slice(indexOfFirst, indexOfLast)
to get only the users for the current page. The table’s <thead>
is always rendered so the header stays visible. We render a button for each page number by computing Math.ceil(users.length/itemsPerPage)
pages. Clicking a page button sets currentPage
, which re-slices and shows that page.
Approach 2: Using react-paginate
The react-paginate
library provides a pre-built pagination UI with next/prev links and optional page-size dropdown. Install it with npm install react-paginate
. In your component, import it and use useState
for a zero-based page
index. For example:
import ReactPaginate from 'react-paginate';
// ... inside component:
const [page, setPage] = useState(0); // zero-based page index for react-paginate
const itemsPerPage = 5;
const pageCount = Math.ceil(users.length / itemsPerPage);
const currentUsers = users.slice(page * itemsPerPage, (page + 1) * itemsPerPage);
return (
<>
<table>…</table>
<ReactPaginate
pageCount={pageCount}
pageRangeDisplayed={3}
marginPagesDisplayed={1}
onPageChange={({ selected }) => setPage(selected)} // event.selected is 0-based:contentReference[oaicite:8]{index=8}
containerClassName="pagination"
activeClassName="active"
previousLabel="Previous"
nextLabel="Next"
/>
</>
);
JavaScriptHere pageCount={Math.ceil(users.length/itemsPerPage)}
defines how many pages there are. The onPageChange
prop is a function that receives an event object; use event.selected
to update the page state. We pass CSS class names via containerClassName
and activeClassName
(e.g. .pagination
, .active
) to style the controls.
Finally, render the table body using currentUsers
as before, and place the <ReactPaginate>
bar below it. The CSS classes allow styling of the pagination bar – for example, .active
highlights the current page. The output UI will show numbered pages with “Previous/Next” buttons and ellipses if pages are many (as illustrated above).
Adapting for Server-Side Pagination
For large datasets, fetch only one page of data at a time. Modify the above as follows: use a useEffect
that depends on [currentPage]
and fetches only that page from the API. For example:
useEffect(() => {
fetch(`/api/users?page=${currentPage}&limit=${itemsPerPage}`)
.then(res => res.json())
.then(data => setUsers(data.items)); // data.items is the page content
}, [currentPage]);
JavaScript- Replace the client-side
.slice()
logic: instead of slicing a full array, let the server return exactly theusers
for the current page. - Ensure
useEffect
watchescurrentPage
so a fetch runs on every page change. - In the custom solution,
users
will now just be one page of rows, and you can compute page numbers if your API returns a total count. - In the
react-paginate
solution, keep the sameonPageChange
handler but simply updatecurrentPage
and setusers
from the response. Notereact-paginate
is zero-indexed by default, so if your API expects page=1 for the first page, add 1 topage
when building the URL (e.g.page=${page+1}
). - Use query params like
?page=2&limit=5
as needed.
These adjustments ensure only the needed records are fetched per page. All other UI logic remains the same: the table header is static, and page numbers still use setCurrentPage
or setPage
via onPageChange
to drive the next fetch.
Conclusion
Pagination in dynamic React tables doesn’t require complex dependencies or boilerplate. In this guide, we covered two effective approaches:
- A custom pagination solution using React hooks (
useState
,useEffect
) to slice data locally and control page navigation. - A cleaner UI-driven approach using the
react-paginate
library for managing pagination logic and rendering controls.
Both methods support a static header and seamless user experience. We also outlined how to adapt each approach for server-side pagination, where only the current page’s data is fetched from the backend using query parameters like ?page=2&limit=5
.
No matter which model you choose, the key is separating page state from the full dataset and rendering only what’s needed—either via .slice()
for local data or backend filtering for large datasets. This ensures scalability, better performance, and simpler state management.
FAQs:
How do I paginate a table in React without using a library?
You can paginate a table in React using useState to track the current page and .slice() to render only the relevant data per page. Combine it with a custom pagination control (e.g. page number buttons) that updates the current page on click.
What is the difference between client-side and server-side pagination in React?
Client-side pagination loads the entire dataset in the browser and paginates using JavaScript. Server-side pagination fetches only a subset (e.g. limit=5&page=2
) from the backend, making it suitable for large datasets or APIs with built-in pagination.
How does react-paginate work with dynamic tables in React?
react-paginate is a UI component that handles page navigation logic. You bind its onPageChange to update your current page state and slice the data accordingly for display. For server-side pagination, trigger an API call in the onPageChange handler.
How do I keep the table header fixed while paginating in React?
Make sure your is outside of the paginated data logic. Only the should be affected by slicing or fetched pages. This way, the table header stays intact across all page changes.
How can I integrate pagination if my backend already supports it?
Skip local slicing. Instead, fetch data directly for the current page using query parameters like ?page=2&limit=5
. Update your table content with the new data upon each page change. Both custom pagination and react-paginate
support this model with minor adjustments.