A good[ish] website
Web development blog, loads of UI and JavaScript topics
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 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.
JavaScript has Array.prototype.sort()
, here’s a brief reminder on how that works.
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:
reverse()
though).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:
compareFunction(a, b)
returns less than 0
, sort a
to an index lower than b
(i.e. a
comes first).compareFunction(a, b)
returns 0
, leave a
and b
unchanged with respect to each other, but sorted with respect to all different elements.compareFunction(a, b)
returns greater than 0
, sort b
to an index lower than a
(i.e. b
comes first).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'
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.
Here’s how the sortable table should look like:
Name | Description |
---|---|
Aluminum siding | Building material |
Chair | Sitting device |
Cleanex | Tissue |
Jade buddha | Trinket |
Knob | Adjective |
Sock | Clothing item |
Tangerine | Fruit |
Tube | Topological shape |
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):
The component that holds the arrow has some accessibility requirements:
button
, since that’s the semantically correct element to use in a case like this.Here’s the component that renders the sort button:
import { BlankButton, VisuallyHidden } from './styles'
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.
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:
data
array[config]
objectAnd the return value:
items
arrayrequestSort()
fnonClick
event happens in a sort button etc.key
stringsortConfig
object{ 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 useSortableData
This 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'
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
Remember to use <caption>
with tables, that’s good for accessibility.
Comments would go here, but the commenting system isn’t ready yet, sorry.