React Table Server Side Pagination with Sorting and Search Filters

After following [a post](https://dev.to/elangobharathi/server-side-pagination-using-react-table-v7-and-react-query-v3-3lck) on dev.to I was able to setup a basic table with React Table Server Side Pagination. However since the post did not have the sorting and search features I had to extend it and hence this one!

– You may find full code here at my github repo

Here is what my final table with React Table Server Side Pagination looks like:

Let’s start with some initial imports. My example uses `react-query` so make sure you have it installed. It is a great library anyway. I also use `axios` library for making ajax calls.

import React, {useState, useEffect, useMemo} from "react"
import { useTable, usePagination, useSortBy } from "react-table"
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
import axios from 'axios'

Next I import user columns which I place in another file named `columns.jsx`
import { USERS_COLUMNS } from "./columns"

And here are the contents of `columns.jsx` file:
export const USERS_COLUMNS = [
{
Header: "Email",
accessor: "email",
},
{
Header: "Name",
accessor: "name",
},
{
Header: "Phone",
accessor: "phone",
},
{
Header: "Role",
accessor: "role",
},
{
Header: "Employee Number",
accessor: "employee_number"
},
]

Next imports are:
import SortIcon from 'mdi-react/SortIcon'
import SortAscendingIcon from 'mdi-react/SortAscendingIcon'
import SortDescendingIcon from 'mdi-react/SortDescendingIcon'
import ReactTablePagination from '@/shared/components/table/components/ReactTablePagination'
import UsersFilter from "./UsersFilter"

Let me explain it a bit. First three imports are icons used for sorting. Next to it is `ReactTablePagination` component I have created for pagination links and last one `UsersFilter` is the search area where I place search box with a submit link. I may also want to add more filters later on.

I will post `ReactTablePagination` and `UsersFilter` code down the page. Let’s first work with our current `UsersIndex.jsx` file and its main component `DataTable` but before that let me post some declarations I have made outside of `DataTable` component.

Okay, once all the imports are done at the top of this page. Let start with structure of rest of this file.

Since I am using `react-query`, and you should also consider using it if your app is doing ajax requests for data extensively, I will wrap my DataTable component within `QueryClientProvider` which is exported from `react-query` library if you noticed it at the top of the page.

So after imports I initialise the queryClient

const queryClient = new QueryClient()

… and wrap my `DataTable` with `QueryClientProvider` by passing client to it and export it at the end of the page. You may also consider to wrap you main <App> within this client, I have just added it in my this one page only.

This is the overall structure of UsersIndex.jsx file

... imports at the top of the file

const queryClient = new QueryClient()

... other file code

const DataTable = () => {
... component code
}

const TableWrapper = () => {
return (
<QueryClientProvider client={queryClient}>
<DataTable />
</QueryClientProvider>
)
}

export default TableWrapper;

Lets dive into the `…other file code first`. This is the code which is before the main `DataTable` component.
const initialState = {
queryPageIndex: 0,
queryPageSize: 10,
totalCount: 0,
queryPageFilter:"",
queryPageSortBy: [],
};

const PAGE_CHANGED = 'PAGE_CHANGED'
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED'
const PAGE_SORT_CHANGED = 'PAGE_SORT_CHANGED'
const PAGE_FILTER_CHANGED = 'PAGE_FILTER_CHANGED'
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED'

const reducer = (state, { type, payload }) => {
switch (type) {
case PAGE_CHANGED:
return {
...state,
queryPageIndex: payload,
};
case PAGE_SIZE_CHANGED:
return {
...state,
queryPageSize: payload,
};
case PAGE_SORT_CHANGED:
return {
...state,
queryPageSortBy: payload,
};
case PAGE_FILTER_CHANGED:
return {
...state,
queryPageFilter: payload,
};
case TOTAL_COUNT_CHANGED:
return {
...state,
totalCount: payload,
};
default:
throw new Error(`Unhandled action type: ${type}`)
}
};

const fetchUsersData = async (page, pageSize, pageFilter, pageSortBy) => {
let paramStr = ''
if( pageFilter.trim().length > 1 ) {
paramStr = `&keyword=${pageFilter}`
}
if( pageSortBy.length > 0 ) {
const sortParams = pageSortBy[0];
const sortyByDir = sortParams.desc ? 'desc' : 'asc'
paramStr = `${paramStr}&sortby=${sortParams.id}&direction=${sortyByDir}`
}
try {
const response = await axios.get(
`/users?page=${page+1}&limit=${pageSize}${paramStr}`
);
const results = response.data.data;
const data = {
results: results,
count: response.data.total
};
return data;
} catch (e) {
throw new Error(`API error:${e?.message}`)
}
}

New thing to notice in the code above is the use of reducer. If you are not sure how reducers work you should check [this post](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers) or a simplified [post here](https://www.robinwieruch.de/javascript-reducer/)

Also there is _fetchUsersData_ function which is responsible for fetching user data and most of it is self-explaining.

React Table Server Side Pagination Component

And finally here is the React Table Server Side Pagination `DataTable` component

const DataTable = () => {
const [keyword, setKeyword] = useState('');
const [useFilter, setUseFilter] = useState(false);
const onClickFilterCallback = ( filter ) => {
if(filter.trim() === "") {
alert('Please enter a keyword to search!')
return
}
if(filter === keyword) {
alert('No change in search')
return
}
setUseFilter(true)
setKeyword(filter)
}

let columns = useMemo( () => USERS_COLUMNS, [])

const [{ queryPageIndex, queryPageSize, totalCount, queryPageFilter, queryPageSortBy }, dispatch] =
useReducer(reducer, initialState);

const { isLoading, error, data, isSuccess } = useQuery(
['users', queryPageIndex, queryPageSize, queryPageFilter, queryPageSortBy],
() => fetchUsersData(queryPageIndex, queryPageSize, queryPageFilter, queryPageSortBy),
{
keepPreviousData: false,
staleTime: Infinity,
}
);

const totalPageCount = Math.ceil(totalCount / queryPageSize)

const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
page,
pageCount,
pageOptions,
gotoPage,
previousPage,
canPreviousPage,
nextPage,
canNextPage,
setPageSize,
state: { pageIndex, pageSize, sortBy }
} = useTable({
columns,
data: data?.results || [],
initialState: {
pageIndex: queryPageIndex,
pageSize: queryPageSize,
sortBy: queryPageSortBy,
},
manualPagination: true,
pageCount: data ? totalPageCount : null,
autoResetSortBy: false,
autoResetExpanded: false,
autoResetPage: false
},
useSortBy,
usePagination,
);
const manualPageSize = []

useEffect(() => {
dispatch({ type: PAGE_CHANGED, payload: pageIndex });
}, [pageIndex]);

useEffect(() => {
dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize });
gotoPage(0);
}, [pageSize, gotoPage]);

useEffect(() => {
dispatch({ type: PAGE_SORT_CHANGED, payload: sortBy });
gotoPage(0);
}, [sortBy, gotoPage]);

useEffect(() => {
if ( useFilter ) {
dispatch({ type: PAGE_FILTER_CHANGED, payload: keyword });
gotoPage(0);
}
}, [keyword, gotoPage, useFilter]);

useEffect(() => {
if (data?.count) {
dispatch({
type: TOTAL_COUNT_CHANGED,
payload: data.count,
});
}
}, [data?.count]);

if (error) {
return <p>Error</p>;
}

if (isLoading) {
return <p>Loading...</p>;
}
if(isSuccess)
return (
<>
<div className='table react-table'>
<form className="form form--horizontal">
<div className="form__form-group">
<div className="col-md-9 col-lg-9">
<UsersFilter onClickFilterCallback={onClickFilterCallback} defaultKeyword={keyword} />
</div>
<div className="col-md-3 col-lg-3 text-right pr-0">
<Link style={{maxWidth:'200px'}}
className="btn btn-primary account__btn account__btn--small"
to="/users/add"
>Add new user
</Link>
</div>
</div>
</form>
{
typeof data?.count === 'undefined' && <p>No results found</p>
}
{data?.count &&
<>
<table {...getTableProps()} className="table">
<thead>
{headerGroups.map( (headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map( column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{column.isSorted ? <Sorting column={column} /> : ''}
</th>
))}
</tr>
))}
</thead>
<tbody className="table table--bordered" {...getTableBodyProps()}>
{page.map( row => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{
row.cells.map( cell => {
return <td {...cell.getCellProps()}><span>{cell.render('Cell')}</span></td>
})
}
</tr>
)
})}
</tbody>
</table>
</>
}
</div>
{(rows.length > 0) && (
<>
<ReactTablePagination
page={page}
gotoPage={gotoPage}
previousPage={previousPage}
nextPage={nextPage}
canPreviousPage={canPreviousPage}
canNextPage={canNextPage}
pageOptions={pageOptions}
pageSize={pageSize}
pageIndex={pageIndex}
pageCount={pageCount}
setPageSize={setPageSize}
manualPageSize={manualPageSize}
dataLength={totalCount}
/>
<div className="pagination justify-content-end mt-2">
<span>
Go to page:{' '}
<input
type="number"
value={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>
</>
)}
</>
)
}

And there is one helper component which is outside of `DataTable` component. I just placed it at the bottom, just before the `TableWrapper`.
const Sorting = ({ column }) => (
<span className="react-table__column-header sortable">
{column.isSortedDesc === undefined ? (
<SortIcon />
) : (
<span>
{column.isSortedDesc
? <SortAscendingIcon />
: <SortDescendingIcon />}
</span>
)}
</span>
);

It is not possible to explain every line and I hope the code makes sense to you. There is one thing I want to mention though. Notice the last three settings in the block:
manualPagination: true,
pageCount: data ? totalPageCount : null,
autoResetSortBy: false,
autoResetExpanded: false,
autoResetPage: false

I had to set them in order to get rid of “Maximum update depth exceeded” error after I turned manualPagination on and implemented server side pagination with sorting and search in my reactjs application. ([See ref here](https://github.com/tannerlinsley/react-table/issues/2369))

– Full code here at my github repo.

Leave a Reply