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 }) =& gt; {
  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) =& gt; {
  let paramStr = ''
  if (pageFilter.trim().length & gt; 1 ) {
    paramStr = `&amp;keyword=${pageFilter}`
  }
  if (pageSortBy.length & gt; 0 ) {
    const sortParams = pageSortBy[0];
    const sortyByDir = sortParams.desc ? 'desc' : 'asc'
    paramStr = `${paramStr}&amp;sortby=${sortParams.id}&amp;direction=${sortyByDir}`
  }
  try {
    const response = await axios.get(
      `/users?page=${page + 1}&amp;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' & amp;& amp; < p >No results found </p>
}
          {
            data?.count & amp;& amp;
          <>
            <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) & amp;& amp; (
        <>
          < 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