React Table Server Side Pagination with Sorting and Search Filters
Tags: react, javascript, table, serverside
Published: 2022-01-04 06:53:01

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 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](https://github.com/inimist/react-table-serverside-pagination/)

Here is what my final table looks like:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j91isc583g4gz39kr3s.jpg)

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. 

```javascript
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`

```javascript
import { USERS_COLUMNS } from "./columns"
```

And here are the contents of `columns.jsx` file:

```javascript
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:

```javascript
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

```javascript
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

```javascript

... 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.

```javascript
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.

And finally here is the `DataTable` component

```javascript
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`.

```javascript
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:

```javascript
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](https://github.com/inimist/react-table-serverside-pagination/).