# How to make a sortable data table using React

Make your data tables pop more, by making the data in them more friendly by
means of sortable columns, all the while keeping things accessible and
responsive.

How the table sorting actually works is: **sort the data that makes the table**,
and let React re-render the table with the newly sorted data.

## The demo table and the data

The very ordinary looking demo table: `<SortableTable />`, which renders random
things in random order:

[Interactive example](/sortable-table)

Here’s the data we’re using:

```js
const randomThings = [
  { name: 'Sock', description: 'Clothing item' },
  { name: 'Tube', description: 'Topological shape' },
  { name: 'Chair', description: 'Sitting device' },
  { name: 'Aluminum siding', description: 'Building material' },
  { name: 'Cleanex', description: 'Tissue' },
  { name: 'Jade buddha', description: 'Trinket' },
  { name: 'Knob', description: 'Adjective' },
  { name: 'Tangerine', description: 'Fruit' },
]
```

And here’s the component itself:

```jsx
const SortableTable = () => (
  <table>
    <caption align='bottom'>List of random things</caption>
    <thead>
      <tr>
        <th>Name</th>
        <th>Description</th>
      </tr>
    </thead>
    <tbody>
      {tableData.map(thing => (
        <tr key={thing.name}>
          <td>{thing.name}</td>
          <td>{thing.description}</td>
        </tr>
      ))}
    </tbody>
  </table>
)
```

Nothing surprising so far.

## Sorting the data with sort

JavaScript has `Array.prototype.sort()`, here’s a brief reminder on how that
works. For a broader reference to adding, removing, and sorting array values,
see [working with JavaScript arrays](/working-with-javascript-arrays).

### How to use the native JavaScript sort method

By default `sort` will sort the array alphabetically, all non `undefined` values
are converted into strings. No need to use the compare function:

```js
const things = ['Zumba', 'Aardvark']
const numbers = [9, 1, 100]
console.log(numbers.sort(), things.sort())
// [1, 100, 9] ['Aardvark', 'Zumba']
```

The default behavior is great for simple use-cases, **but we can immediately see
few limitations**:

- 100 comes before 9, that’s because **the sort is alphabetical**, not numeric.
- It only sorts in ascending order (the result can be run through `reverse()`
  though).
- This will not work with arrays of objects, like in our test data.

The native sort method runs the compare function under the hood, the first
attribute in sort function:

```js
things.sort((a, b) => {
  if (a < b) return -1
  if (a > b) return 1
  return 0
})
```

Here’s how that `compareFunction` works:

- If `compareFunction(a, b)` **returns less than `0`**, sort `a` to an index
  lower than `b` (i.e. `a` comes first).
- If `compareFunction(a, b)` **returns `0`**, leave `a` and `b` unchanged with
  respect to each other, but sorted with respect to all different elements.
- If `compareFunction(a, b)` **returns greater than `0`**, sort `b` to an index
  lower than `a` (i.e. `b` comes first).

### Sorting the table data

Now that we know how sort works, we can build a simple sorting helper function.
It takes two params: the **array** to be sorted, and the **key** that we want to
sort the data by:

```js
const sortTableData = (array, sortBy) => {
  return array.sort((a, b) => {
    if (a[sortBy] < b[sortBy]) return -1
    if (a[sortBy] > b[sortBy]) return 1
    return 0
  })
}

const things = [
  { name: 'Zumba', description: 'Sport' },
  { name: 'Aardvark', description: 'Animal' },
]

console.log(sortTableData(things, 'name'))
// { name: 'Aardvark', description: 'Animal' }
```

An elementary feature for a sorting method is its ability change direction:
**ascending** vs **descending**. Ascending being the default in this case:

```js
const sortTableData = (array, { sortBy, direction }) => {
  return array.sort((a, b) => {
    if (a[sortBy] < b[sortBy]) return direction === 'ascending' ? -1 : 1
    if (a[sortBy] > b[sortBy]) return direction === 'ascending' ? 1 : -1
    return 0
  })
}

console.log(sortTableData(things, { sortBy: 'name', direction: 'descending' }))
```

Let’s bake that into our demo component, it now takes `sortConfig` as a prop:

```js
<SortableTable sortConfig={{ sortBy: 'name', direction: 'ascending' }} />
```

Now it renders a table sorted by the "Name" column:

[Interactive example](/sortable-table)

The innards of the table component look like this (it’s using the above
`sortTableData` helper):

```jsx
import sortTableData from '../helpers/sortTableData'
import things from './things.ts'

const SortableTable = ({ sortConfig }) => {
  const shouldSort = sortConfig?.sortBy
  const tableData = shouldSort ? sortTableData(things, sortConfig) : things

  return (
    <table>
      <caption align='bottom'>List of random things</caption>
      <thead>
        <tr>
          <th>Name</th>
          <th>Description</th>
        </tr>
      </thead>
      <tbody>
        {tableData.map(thing => (
          <tr key={thing.name}>
            <td>{thing.name}</td>
            <td>{thing.description}</td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}
```

That works, but it’s very rudimentary; only one row can be chosen to be sorted,
and there’s no sorting UI.

Next, **let’s build a proper sorting UI**.

## Make the individual table rows sortable

Here’s how the sortable table should look like:

[Interactive example](/sortable-table)

### The sort button

As sort button icons I’m using some Unicode arrows (in production code with
actual design requirements, you’d probably use a some nice SVG icons carefully
crafted by your team’s designer):

- ↕︎ = sorting is available
- ↓ = ascending sort
- ↑ = descending sort

The component that holds the arrow has some accessibility requirements:

- **It should be a `button`**, since that’s the semantically correct element to
  use in a case like this.
- **The buttons should hold screen-reader-only text**, which reads out the
  functionality of the button, i.e.: "Sort row ascending".
- **It shouldn’t look like a button**. The button can be reset to look like
  text.

[How to make a visually hidden React component](/visually-hidden)

[How to make a button not look like a button](/button-reset)

Here’s the component that renders the sort button:

```jsx
import { BlankButton, VisuallyHidden } from './styles.tsx'

const SortButton = ({ direction, id, onClick, sortBy }) => {
  const arrows = { ascending: '↓', descending: '↑' }
  const arrow = sortBy === id ? arrows[direction] : '↕︎'

  return (
    <BlankButton id={id} onClick={onClick}>
      {arrow}
      <VisuallyHidden>Sort {direction}</VisuallyHidden>
    </BlankButton>
  )
}
```

The component now looks something like this:

```jsx
const SortableTable = ({ sortConfig }) => {
  const [sortedItems, setSortedItems] = React.useState(things)
  const [direction, setDirection] = React.useState()
  const [sortBy, setSortBy] = React.useState()
  const { showSortUi } = sortConfig || {}

  const handleClick = event => {
    const sortDir = direction === 'descending' ? 'ascending' : 'descending'
    setDirection(sortDir)
    setSortBy(event.target.id)
    const sortConfig = { sortBy: event.target.id, direction: sortDir }
    setSortedItems(sortTableData(things, sortConfig))
  }

  return (
    <table>
      <caption align='bottom'>List of random things</caption>
      <thead>
        <tr>
          <th>
            Name{' '}
            {showSortUi && (
              <SortButton
                direction={direction}
                id='name'
                onClick={handleClick}
                sortBy={sortBy}
              />
            )}
          </th>
          <th>
            Description{' '}
            {showSortUi && (
              <SortButton
                direction={direction}
                id='description'
                onClick={handleClick}
                sortBy={sortBy}
              />
            )}
          </th>
        </tr>
      </thead>
      <tbody>
        {sortedItems.map(thing => (
          <tr key={thing.name}>
            <td>{thing.name}</td>
            <td>{thing.description}</td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}
```

That might work for you, but if you find yourself reusing it a lot, might come
handy to write a React hook.

## A sort React hook

As I was researching this article I came across a
[Smashing Magazine article](https://www.smashingmagazine.com/2020/03/sortable-tables-react/)
on the same topic and it had brilliant solution baked into a hook. I’m going to
use a slightly modified version of that.

Here’s how the `useSortableData` hooks might look when used in a React
component:

```js
const { items, requestSort, sortConfig } = useSortableData(data, {
  direction: 'ascending',
  key: 'name',
})
```

Parameters:

`data` array

The data you want to sort.

`[config]` object

The optional config object defines the initial sort, if you want to use the
    original order when the table loads, then skip the config completely.

And the return value:

`items` array

The sorted data.

`requestSort()` fn

This method is used to trigger the sort "on demand", you call it when an
    `onClick` event happens in a sort button etc.

`key` string

The object key to sort by.

`sortConfig` object

This object contains the current direction of the sort and the key that is
    being sorted. i.e.:

``

And the code for the hook itself, scroll down to see how it’s used:

```js
import React from 'react'

/**
 * Sorts an array og objects, either in ascending or descending order, based on
 * the given key.
 *
 * @param {array} array - The array to sort.
 * @param {object} config - The configuration object.
 * @param {string} config.key - The object key to sort by.
 * @param {(ascending|descending)} [config.direction] - The sort direction.
 * @returns {array}
 */
const sortTableData = (array, { key, direction }) => {
  return array.sort((a, b) => {
    if (a[key] < b[key]) return direction === 'ascending' ? -1 : 1
    if (a[key] > b[key]) return direction === 'ascending' ? 1 : -1

    return 0
  })
}

/**
 * A React hook that will sort a given array. The configuration object can
 * define the initial sort order and key, if none given, no sort is done.
 *
 * @param {array} items - The data that needs to be sorted.
 * @param {object} [config] - A configuration object.
 * @param {string} [config.key] - Name of key to sort by.
 * @param {(ascending|descending)} [config.direction] - The sort direction.
 * @returns {object}
 */
const useSortableData = (items = [], config) => {
  const [sortConfig, setSortConfig] = React.useState(config)

  const sortedItems = React.useMemo(() => {
    // If no config was defined then return the unsorted array
    if (!sortConfig) return items

    return sortTableData(items, { ...sortConfig })
  }, [items, sortConfig])

  const requestSort = key => {
    let direction = 'descending'

    if (
      sortConfig &&
      sortConfig.key === key &&
      sortConfig?.direction === 'descending'
    ) {
      direction = 'ascending'
    }

    setSortConfig({ key, direction })
  }

  return { items: sortedItems, requestSort, sortConfig }
}

export default useSortableData
```

This is how you'd use the `useSortableData` hook to make a table:

```jsx
import React from 'react'
import PropTypes from 'prop-types'
import VisuallyHidden from '../src/components/VisuallyHidden'
import useSortableData from '../src/hooks/useSortableData'
import { BlankButton } from './styles.tsx'

const SortButton = ({ direction, id, onClick, sortBy }) => {
  const arrows = { ascending: '↓', descending: '↑' }
  const arrow = sortBy === id ? arrows[direction] : '↕︎'

  return (
    <BlankButton id={id} onClick={onClick}>
      {arrow}
      <VisuallyHidden>Sort {direction}</VisuallyHidden>
    </BlankButton>
  )
}

const SortableTableWithHook = props => {
  const { items, requestSort, sortConfig } = useSortableData(
    randomThings,
    props.config
  )

  return (
    <table>
      <caption align='bottom'>List of random things</caption>
      <thead>
        <tr>
          <th>
            Name{' '}
            <SortButton
              direction={sortConfig?.direction}
              id='name'
              onClick={() => requestSort('name')}
              sortBy={sortConfig?.key}
            />
          </th>
          <th>
            Description{' '}
            <SortButton
              direction={sortConfig?.direction}
              id='description'
              onClick={() => requestSort('description')}
              sortBy={sortConfig?.key}
            />
          </th>
        </tr>
      </thead>
      <tbody>
        {items.map(thing => (
          <tr key={thing.name}>
            <td>{thing.name}</td>
            <td>{thing.description}</td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}

export default SortableTableWithHook
```

## Conclusions

Remember to use `<caption>` with tables, that’s good for accessibility.
