How to make tables paginated in React
Pagination improves performance and user experience when displaying large datasets by breaking data into manageable chunks and reducing initial render load. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented pagination in countless data tables throughout my 11 years of frontend development. The most efficient approach is managing current page state and slicing the data array to display only the relevant subset. This method provides responsive pagination controls with clear navigation and maintains optimal rendering performance for large datasets.
Track current page in state, calculate page slice indices, and render pagination controls with next/previous navigation.
import { useState } from 'react'
const PaginatedTable = ({ data }) => {
const [currentPage, setCurrentPage] = useState(1)
const itemsPerPage = 10
const totalPages = Math.ceil(data.length / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const currentData = data.slice(startIndex, endIndex)
const goToPage = (page) => {
setCurrentPage(Math.max(1, Math.min(page, totalPages)))
}
const renderPageNumbers = () => {
const pages = []
for (let i = 1; i <= totalPages; i++) {
if (
i === 1 ||
i === totalPages ||
(i >= currentPage - 1 && i <= currentPage + 1)
) {
pages.push(
<button
key={i}
onClick={() => goToPage(i)}
className={currentPage === i ? 'active' : ''}
>
{i}
</button>
)
} else if (pages[pages.length - 1]?.key !== 'ellipsis') {
pages.push(<span key={`ellipsis-${i}`}>...</span>)
}
}
return pages
}
return (
<div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{currentData.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
))}
</tbody>
</table>
<div className='pagination'>
<button
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
>
Previous
</button>
{renderPageNumbers()}
<button
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
<span>
Page {currentPage} of {totalPages} ({data.length} total items)
</span>
</div>
</div>
)
}
Here the currentPage state tracks which page is currently displayed. The totalPages calculation determines how many pages are needed based on total data length and itemsPerPage. The slice method extracts only the data subset for the current page using calculated startIndex and endIndex. The goToPage function ensures page number stays within valid bounds using Math.max and Math.min. The renderPageNumbers function creates smart pagination showing first page, last page, current page with neighbors, and ellipsis for gaps. Previous and Next buttons are disabled at boundaries to prevent invalid navigation.
Best Practice Note:
This is the pagination pattern we use in CoreUI table components for efficient data navigation in enterprise dashboards. Combine pagination with sorting and filtering by applying those operations before calculating pagination indices, implement URL-based pagination using query parameters to enable shareable links to specific pages, and consider virtual scrolling or infinite scroll for very large datasets where traditional pagination becomes cumbersome.



