14
React Table Server Side Pagination with Sorting and Search Filters
After following a post on dev.to I was able to setup a basic server side paginated table. 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 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 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 or a simplified post here
Also there is fetchUsersData function which is responsible for fetching user data and most of it is self-explaining.
And finally here is the 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)
- Full code here at my github repo.
14