Implementing Custom Pagination Logic with React and Tanstack Table
Enhance your data tables with customized pagination strategies
Jul 22, 2024 - 20:56 • 5 min read
One of the essential elements for any dynamic web application is presenting data in an accessible and user-friendly manner. Often, this involves utilizing tables to display large datasets, requiring effective pagination to enhance user experience. In this blog post, we will explore how to implement custom pagination logic in a data table using React and Tanstack Table, a powerful table component library.
Why Custom Pagination?
Default pagination mechanisms work well for most applications, but sometimes you need tailored functionalities not provided by out-of-the-box solutions. Custom pagination allows you to cater specifically to your application’s needs, manage server-side paginations, and optimize client-side performance.
Setting Up Tanstack Table
First, let’s set up Tanstack Table in our React application. If you haven’t already, you’ll need to install the packages:
npm install @tanstack/react-table
Once installed, we can initiate Tanstack Table and configure basic columns and data states:
import React, { useMemo } from 'react';
import { useTable, usePagination } from '@tanstack/react-table';
const DataTable = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page, // Use the page, instead of rows, to render the table
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
},
usePagination
);
return (
<>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>;
})}
</tr>
);
})}
</tbody>
</table>
<div className="pagination">
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'<<'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'<'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'>'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'>>'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Jump to:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
gotoPage(page);
}}
style={{ width: '100px' }}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</>
);
};
export default DataTable;
Custom Pagination Logic
Now that we have a basic table with pagination set up, let's move on to implementing custom pagination logic. Let's say, for example, you want to implement server-side pagination. This would involve fetching data from an API endpoint based on the current page and page size.
First, we need to modify our table component to handle server-side data fetching:
import React, { useEffect, useState, useMemo } from 'react';
import { useTable, usePagination } from '@tanstack/react-table';
const DataTable = ({ columns, fetchData, pageCount: controlledPageCount }) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [pageCount, setPageCount] = useState(controlledPageCount);
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
manualPagination: true,
pageCount,
},
usePagination
);
useEffect(() => {
const fetchDataAsync = async () => {
setLoading(true);
const fetchedData = await fetchData({ pageIndex, pageSize });
setData(fetchedData.data);
setPageCount(fetchedData.pageCount);
setLoading(false);
};
fetchDataAsync();
}, [fetchData, pageIndex, pageSize]);
return (
<>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{loading ? (
<tr>
<td colSpan="10000">Loading...</td>
</tr>
) : (
page.map(row => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>;
})}
</tr>
);
})
)}
</tbody>
</table>
<div className="pagination">
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'<<'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'<'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'>'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'>>'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Jump to:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
gotoPage(page);
}}
style={{ width: '100px' }}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</>
);
};
export default DataTable;
Fetching Data from an API
Next, we need to set up a function to fetch data from an API. This function will be passed as the fetchData
prop to the DataTable
component:
const fetchData = async ({ pageIndex, pageSize }) => {
const response = await fetch(
`/api/data?page=${pageIndex}&size=${pageSize}`
);
const data = await response.json();
return {
data: data.items,
pageCount: data.totalPages,
};
};
You can adjust the API URL to match your backend setup. Ensure that your API paginates data and returns both the fetched items and the total number of pages.
Putting It All Together
Finally, let’s render our custom paginated table in the main component:
const columns = useMemo(
() => [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Age', accessor: 'age' },
],
[]
);
const App = () => {
return (
<div>
<h1>Custom Paginated Table</h1>
<DataTable columns={columns} fetchData={fetchData} pageCount={10} />
</div>
);
};
export default App;
Conclusion
Implementing custom pagination in React using the Tanstack Table library allows for a flexible and efficient way to handle large datasets. Whether you’re dealing with client-side or server-side data, the ability to tailor your pagination logic provides a significant advantage in creating responsive and user-friendly applications. This example demonstrates just one approach to customizing pagination, but the same principles can be applied to other unique needs and scenarios.