Global Filtering and Sorting with React Table

This post is a continuation of my article on Using React Query with React Table. I recommend reading that first to understand the full context.

We will also be following along with the same set of example code which can be found at github.com/nafeu/react-query-table-sandbox. We will be focusing on the ReactTableFilterSort.jsx file.

You can get it quickly set up with:

git clone https://github.com/nafeu/react-query-table-sandbox.git
cd react-query-table-sandbox
npm install
npm start

What We Are Building

Understanding Our Data

In the example code, I prepared a mock api which returns data for an imaginary software dev discussion group website. The data is formatted like so:

[
  {
    "id": 1,
    "name": "How to use react-table in reporting dashboard",
    "active": 40,
    "status": "locked",
    "upvotes": 30
  },
  {
    "id": 2,
    "name": "How to use react-query for BI solution",
    "active": 31,
    "status": "resolved",
    "upvotes": 39
  }
  ...
]

For the purpose of this tutorial, our data will not get any more complex than this. How can we now leverage React Table to implement a global filter and sortability into our table? We will do so by modifying our existing ReactQueryWithTable example. Let’s get started.

Adding a Global Filter Component

First we want to import some additional hooks from the react-table library, namely the useGlobalFilter, useAsyncDebounce and useSortBy hooks:

import {
  useTable,
  useGlobalFilter,
  useAsyncDebounce,
  useSortBy
} from 'react-table';

Then we want to create a new component called GlobalFilter which takes a few React Table defined props and renders a search box for us:

const TWO_HUNDRED_MS = 200;

function GlobalFilter({
  preGlobalFilteredRows,
  globalFilter,
  setGlobalFilter,
}) {
  const [value, setValue] = useState(globalFilter);
  const onChange = useAsyncDebounce(value => {
    setGlobalFilter(value || undefined)
  }, TWO_HUNDRED_MS);

  return (
    <input
      value={value || ""}
      onChange={e => {
        setValue(e.target.value);
        onChange(e.target.value);
      }}
      placeholder={`Search`}
    />
  )
}

Using useState and globalFilter, we create a value state variable which keeps track of what we put into our search box, and we use useAsyncDebounce to call setGlobalFilter with a 200ms input delay.

We can’t really see it here, but setGlobalFilter performs quite a bit of magic. As our globalFilter object is connected to the rest of the table, updates from setGlobalFilter will modify the visible rows in our table.

Updating our TableQuery Component

The only modification we do here is add enabled: !tableData to prevent the table from re-fetching in the background as it can mess up the filtered or sorted order. Implementing re-fetching with a filtered or sorted structure is a more advanced topic that won’t be covered in this article.

const {
  data: apiResponse,
  isLoading
} = useQuery('discussionGroups', fetchData, { enabled: !tableData });

Updating our TableInstance Component

Here we just update the tableInstance instantiation with the hooks useGlobalFilter and useSortBy

const tableInstance = useTable(
  { columns, data },
  useGlobalFilter,
  useSortBy
);

Updating our TableLayout Component

Here is where the bulk of our updates are in comparison to the ReactQueryWithTable.jsx file:

const TableLayout = ({
  getTableProps,
  getTableBodyProps,
  headerGroups,
  rows,
  prepareRow,
  state: { globalFilter },
  visibleColumns,
  preGlobalFilteredRows,
  setGlobalFilter
}) => {
  return (
    <table {...getTableProps()}>
      <thead>
        <tr>
          <th
            colSpan={visibleColumns.length}
          >
            <GlobalFilter
              preGlobalFilteredRows={preGlobalFilteredRows}
              globalFilter={globalFilter}
              setGlobalFilter={setGlobalFilter}
            />
          </th>
        </tr>
        {headerGroups.map(headerGroup => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map(column => (
              <th {...column.getHeaderProps(column.getSortByToggleProps())}>
                {column.render('Header')}
                <span>
                  {column.isSorted
                    ? column.isSortedDesc
                      ? ' ⬇️'
                      : ' ⬆️'
                    : ' ↕️'}
                </span>
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody {...getTableBodyProps()}>
        {rows.map((row, i) => {
          prepareRow(row)
          return (
            <tr {...row.getRowProps()}>
              {row.cells.map(cell => {
                return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
              })}
            </tr>
          )
        })}
      </tbody>
    </table>
  );
}

We pull out the following additional props from our table instance:

state: { globalFilter },
visibleColumns,
preGlobalFilteredRows,
setGlobalFilter

These allow us to access the globalFilter object, info on visibleColumns, the globalFilter setter and the preGlobalFilteredRows helper.

Inside our <thead> we insert a new <tr>, set it’s colSpan to the count of visibleColumns and insert our GlobalFilter component like so:

<tr>
  <th
    colSpan={visibleColumns.length}
  >
    <GlobalFilter
      preGlobalFilteredRows={preGlobalFilteredRows}
      globalFilter={globalFilter}
      setGlobalFilter={setGlobalFilter}
    />
  </th>
</tr>

This is what it renders:

After this <tr> and inside the same <thead>, we include a modified version of the headerGroups like so:

{headerGroups.map(headerGroup => (
  <tr {...headerGroup.getHeaderGroupProps()}>
    {headerGroup.headers.map(column => (
      <th {...column.getHeaderProps(column.getSortByToggleProps())}>
        {column.render('Header')}
        <span>
          {column.isSorted
            ? column.isSortedDesc
              ? ' ⬇️'
              : ' ⬆️'
            : ' ↕️'}
        </span>
      </th>
    ))}
  </tr>
))}

One of the most important things here is the use of column.getSortByToggleProps(), this is what allows us to hook into React Table’s sorting functionality.

Honestly, that is all it takes. We add some extra UI elements in the form of:

<span>
  {column.isSorted
    ? column.isSortedDesc
      ? ' ⬇️'
      : ' ⬆️'
    : ' ↕️'}
</span>

Which display the sorting state. They utilize .isSorted and .isSortedDesc which again, are values provided to us by React Table thanks to useSortBy.

And there you have it, that’s all it takes to implement a global filter and sorting using React Table. We are using all default configurations here, but React Table also allows you to control the algorithms behind sorting and filtering. Hope this has helped.

Happy Coding!