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:
| Name | Description |
|---|---|
| Sock | Clothing item |
| Tube | Topological shape |
| Chair | Sitting device |
| Aluminum siding | Building material |
| Cleanex | Tissue |
| Jade buddha | Trinket |
| Knob | Adjective |
| Tangerine | Fruit |
Here’s the data we’re using:
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:
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.
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:
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:
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 than0, sortato an index lower thanb(i.e.acomes first). - If
compareFunction(a, b)returns0, leaveaandbunchanged with respect to each other, but sorted with respect to all different elements. - If
compareFunction(a, b)returns greater than0, sortbto an index lower thana(i.e.bcomes 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:
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:
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:
<SortableTable sortConfig={{ sortBy: 'name', direction: 'ascending' }} />Now it renders a table sorted by the "Name" column:
| Name | Description |
|---|---|
| Aluminum siding | Building material |
| Chair | Sitting device |
| Cleanex | Tissue |
| Jade buddha | Trinket |
| Knob | Adjective |
| Sock | Clothing item |
| Tangerine | Fruit |
| Tube | Topological shape |
The innards of the table component look like this (it’s using the above
sortTableData helper):
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:
| Name | Description |
|---|---|
| Sock | Clothing item |
| Tube | Topological shape |
| Chair | Sitting device |
| Aluminum siding | Building material |
| Cleanex | Tissue |
| Jade buddha | Trinket |
| Knob | Adjective |
| Tangerine | Fruit |
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.
Here’s the component that renders the sort button:
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:
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 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:
const { items, requestSort, sortConfig } = useSortableData(data, {
direction: 'ascending',
key: 'name',
})Parameters:
dataarray- The data you want to sort.
[config]objectThe 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:
itemsarray- The sorted data.
requestSort()fnThis method is used to trigger the sort "on demand", you call it when an
onClickevent happens in a sort button etc.keystring- The object key to sort by.
sortConfigobjectThis object contains the current direction of the sort and the key that is being sorted. i.e.:
{ key: 'name', direction: 'ascending' }
And the code for the hook itself, scroll down to see how it’s used:
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 useSortableDataThis is how you'd use the useSortableData hook to make a table:
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 SortableTableWithHookConclusions
Remember to use <caption> with tables, that’s good for accessibility.