Implementing React table pagination handling one million records
Implementing React table pagination handling one million records
Have you ever faces a situation where you react application stuck with a loading state or slow when the data starts to grow?. Well, Most of us would have faced this scenario in our work. Here, we will see how to solve it using react table pagination on the server-side to handle one million records.
Since we will use server-side pagination, we need to set up a nodejs server to fetch the data from MongoDB. checkout this article to set up a nodejs server with pagination, or you can create a mock API with data (1 million records)
React Table
React table is a lightweight and extensible data table for react. It’s a headless UI for data tables in react applications. If you’re new to react table, I will suggest getting started with basics and try pagination.
Here’s a simple react table component without pagination
export const BasicTable = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
footerGroups,
rows,
prepareRow,
} = useTable({
columns,
data,
});
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()}>
{rows.map((row) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</>
);
};
useTable
hooks provide all the properties and methods for the table. So, for example, we need to map getTableProps
props to the table jsx
element and take care of all the functionalities.
As you can see, that table is scrollable because of rendering the whole data in the table. To make it a better experience for the users, we can implement pagination.
Pagination
To use pagination in the table, we need usePagination
hooks from react table. import the hook and add it to the useTable
import { useTable, usePagination } from "react-table";
const {
getTableProps,
getTableBodyProps,
headerGroups,
footerGroups,
rows,
prepareRow,
} = useTable(
{
columns,
data,
},
usePagination
);
Once we have pagination hooks, replace the rows
prop with page
prop inside the useTable
hooks because every data inside the table will be based on the page hereafter. So, we need to render pages instead of rows
.
export const BasicTable = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
footerGroups,
page,
prepareRow,
} = 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>
</>
);
};
Once we make the change, our table will render only the first 10 elements from the data. So, it’s time to implement the pagination buttons in the footer to navigate or fetch the next set of data.
To implement navigation for the paginated table, react table provides four properties. They are,
- nextPage
- previousPage
- canPreviousPage
- canNextPage
nextPage
is an action-based prop that we can use inside the onClick
method. canNextPage
returns a boolean to add a condition whether to render the next button or not. For example, if we reach the last page. We can disable the next button based on the canNextPage
prop.
Also usePagination
provides state that returns pageSize
and pageIndex
. we can use it to show the number of items on the page and also for server-side pagination. We will come back to that in the latter part of the article.
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
},
usePagination
);
Here’s the Paginated Table with all the basic functionalities,
React Table Pagination
Everything is great on the in-built pagination of react table. But, there are use-cases where we need to have more control over the component. One of the use-cases is Server-side pagination.
In the above example, we send the whole data to react table component. What if we get the data from the paginated API. We will get the first 20 rows from API, and we need to call API to fetch the next row of data.
How does react table handle this situation? we are discussing it here because this is the common scenario we will face in real product development(not just simple pagination). First, let’s see how to handle this situation in an efficient way.
Here’s the flow for server side pagination in react table,
- Change to Manual Pagination in react table
- Call API whenever
pageIndex
changes
Now, add this property inside useTable
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
setHiddenColumns,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
manualPagination: true,
pageCount: controlledPageCount,
},
usePagination
);
After that add an useEffect
hook, so whenever the pageIndex changes we can fetch the data
React.useEffect(() => {
fetchData && fetchData({ pageIndex, pageSize });
}, [fetchData, pageIndex, pageSize]);
fetchData
function can be a prop from the parent component or the same component. In most cases, we will be abstracting this Table component. So, it will be a prop from the parent component.
Here’s the fetchData
function takes the current element and check if it’s the same as the previous. If not, it will fetch the new data.
const fetchData = useCallback(
({ pageSize, pageIndex }) => {
// console.log("fetchData is being called")
// This will get called when the table needs new data
// You could fetch your data from literally anywhere,
// even a server. But for this example, we'll just fake it.
// Give this fetch an ID
const fetchId = ++fetchIdRef.current;
setLoading(true);
if (fetchId === fetchIdRef.current) {
fetchAPIData({
limit: pageSize,
skip: pageSize * pageIndex,
search: searchTerm,
});
}
},
[searchTerm]
);
Since we have the fetchData
function in the parent component, we use the useCallback
hook that helps us to avoid unnecessary re-render. We also pass the searchTerm
for filtering functionality.
fetchAPIData
function fetches the data and set it in state value
const fetchAPIData = async ({ limit, skip, search }) => {
try {
setLoading(true);
const response = await fetch(
`/companies?limit=${limit}&skip=${skip}&search=${search}`
);
const data = await response.json();
setData(data.data);
setPageCount(data.paging.pages);
setLoading(false);
} catch (e) {
console.log("Error while fetching", e);
// setLoading(false)
}
};
complete source is available here