Let's build a "simple" data table in a web app, starting with the basics.
In the previous post, we summarized the features expected in a modern data table.
By the end of this tutorial, we'll build a table with filtering, sorting, and basic column controls. In the next post, we'll add new requirements and walk through handling them.
This tutorial is aimed at relative beginners to web development, and will keep the set of tools as basic as possible to remain focused. Skip to the end for suggestions for a more serious project.
This article is part of a series:
To follow along with this tutorial, you'll need the following:
To begin, let's quickly generate an empty React app with Create React App. To stay focused, we'll avoid adding any tools or libraries that aren't directly related to React or our table2.
npx create-react-app react-table-demo --use-npm
.react-table-demo
. cd
into it.npm start
(you don't need to run npm install
because create-react-app
already did it).With npm start
still running, we can begin building something.
To get comfortable with the editing experience, we'll create an ordinary table that doesn't do anything special yet.
This means removing most of what's already on the page. We won't be using the spinner.
App.css
to a basic flex container, removing the logo styles.App.js
to just render "Hello, World!" in the middle of the screen.logo.svg
as we aren't using it anymore.import './App.css';function App() {return (<div className='App'><main>Hello, World!</main></div>);}export default App;
Next, we'll put a basic table in place just to get the ball rolling:
src/utils/useData.js
, which generates a random 2D array of data with headers.src/Table.js
, which consumes useData
and puts it in an HTML table
.src/Table.module.css
to make it look nice.Table
component from Table.js
in App.js
Now that we're showing tabular data, let's start adding features to make it more useful.
We could add features like sorting, filtering, column hiding, and so forth ourselves.
While this isn't especially difficult at first, it gets complicated quickly. Instead of walking this well-trodden path ourselves, we'll use a library to deal with the common problems: react-table.
Let's convert our simple HTML <table>
to one that uses react-table
before we go any further.
Luckily, its creators already wrote and excellent tutorial for this part.
To summarize:
npm install react-table
npm start
useData
in useData.js
to the format react-table
expects.Table.js
to setup and call useTable
with the same data we just adapted.It looks like this now:
While there is a great complex example of what react-table
can do for filtering, we'll start with only a simple global filter.
To use it, we'll create an input that hides every row not containing what the user types. This involves just a few steps:
useGlobalFilter
from react-table
useTable
, which adds a setGlobalFilter
function to the instance returned by useTable
.Filter
component, which just calls setGlobalFilter
as the user types.While we're here, let's add sorting. This is even easier:
useSortBy
from react-table
, add it to the arguments to useTable
.column.getSortByToggleProps()
method, which adds click handlers to the headers.Like with filtering, useSortBy
is highly configurable.
You can set a default sort state, allow sorting by multiple columns, reset sorting whenever you need to, customize the sorting method, and more.
+import * as React from 'react';+import styles from './Table.module.css'-import { useTable } from 'react-table';+import Filter from './Filter.js';+import { useTable, useGlobalFilter, useSortBy } from 'react-table';export default function Table({ data: { columns, data } }) {- const reactTable = useTable({ columns, data });+ const reactTable = useTable({+ columns,+ data+ },+ useGlobalFilter,+ useSortBy+ );const {getTableProps,getTableBodyProps,headerGroups,rows,- prepareRow+ prepareRow,+ setGlobalFilter} = reactTable;return (+ <>+ <Filter onChange={setGlobalFilter} /><table {...getTableProps()} className={styles.Table}><thead>{headerGroups.map(group => (<tr {...group.getHeaderGroupProps()}>{group.headers.map(column => (- <th {...column.getHeaderProps()}>+ <th {...column.getHeaderProps(column.getSortByToggleProps())}>{column.render('Header')}+ <span>+ {column.isSorted ? (+ column.isSortedDesc ? ' 🔽' : ' 🔼'+ ): ''}+ </span></th>))}</tr>);})}</tbody></table>+ </>);}
Unsurprisingly, react-table
also has features for column resizing, hiding, and ordering.
For column resizing, we need to tell react-table
how to calculate column widths:
useFlexLayout
(or useBlockLayout
) from react-table
, add to useTable
.useResizeColumns
. Note that this works differently with each layout, and order matters.useResizeColumns
plugin provides a props getter to handle all the logic for this control.Table
component to prevent the sorting and resizing handles from interfering with each other.Now for column hiding: this one doesn't need a plugin; useTable
already sets it up!
ColumnSelector
component, much like we did for Filter
. It takes the list of all columns from useTable(...).allColumns
and provides a checkbox for each.column.getToggleHiddenProps()
to handle this logic for us.import * as React from 'react';import styles from './Table.module.css'import Filter from './Filter.js';-import { useTable, useGlobalFilter, useSortBy } from 'react-table';+import ColumnSelector from './ColumnSelector.js';+import {+ useTable,+ useFlexLayout,+ useGlobalFilter,+ useSortBy,+ useResizeColumns,+} from 'react-table';export default function Table({ data: { columns, data } }) {const reactTable = useTable({columns,data},+ useFlexLayout,useGlobalFilter,- useSortBy+ useSortBy,+ useResizeColumns);const {getTableProps,getTableBodyProps,headerGroups,rows,+ allColumns,prepareRow,setGlobalFilter} = reactTable;return (<>+ <ColumnSelector columns={allColumns} /><Filter onChange={setGlobalFilter} /><table {...getTableProps()} className={styles.Table}><thead>{headerGroups.map(group => (<tr {...group.getHeaderGroupProps()}>{group.headers.map(column => (- <th {...column.getHeaderProps(column.getSortByToggleProps())}>- {column.render('Header')}- <span>- {column.isSorted ? (- column.isSortedDesc ? ' đź”˝' : ' 🔼'- ): ''}- </span>+ <th {...column.getHeaderProps()}>+ <div {...column.getSortByToggleProps()}>+ {column.render('Header')}+ <span>+ {column.isSorted ? (+ column.isSortedDesc ? ' đź”˝' : ' 🔼'+ ): ''}+ </span>+ </div>+ <div {...column.getResizerProps()} className={[styles.ResizeHandle, column.isResizing && styles.ResizeHandleActive].filter(x=>x).join(' ')}>+ ⋮+ </div></th>))}</tr>))}</thead><tbody {...getTableBodyProps()}>{rows.map((row) => {prepareRow(row);return (<tr {...row.getRowProps()}>{row.cells.map(cell => (<td {...cell.getCellProps()}>{cell.render('Cell')}</td>))}</tr>);})}</tbody></table></>);}
Although column ordering also has a plugin, it doesn't provide props for easy controls like the others. As such, we'll skip it for this tutorial.
Here's what it looks like so far (click to interact):
So far we've added a bunch of features, but the data itself is still showing as plain text.
What if we want to color scores by their value, or right-align IDs? Well, react-table
has us covered here too.
Column definitions can include a Cell
function (in fact, Header
can be a function too) which returns anything that's valid JSX. That is, these can be React components.
import * as React from 'react';function randomFrom(array) {return array[Math.floor(Math.random() * array.length)];}/** Make a silly word that rhymes with Goomba (the Mario mushroom enemies) */function sillyWord() {const leadConsonants = ['B', 'D', 'F', 'G', 'L', 'T', 'V', 'Z'];const middles = ['oom', 'oon', 'um', 'un'];const midConsonants = ['b', 'd']const ends = ['a', 'ah', 'u', 'uh', 'o'];const word = randomFrom(leadConsonants) + randomFrom(middles) + randomFrom(midConsonants) + randomFrom(ends);// Try again if we accidentally picked something offensiveif (['Goombah'].includes(word)) return sillyWord();else return word;}function sillyName() {return `${sillyWord()} ${sillyWord().substring(0, 3)}`;}const genericHeaders = ['ID', 'Name', 'Friend', 'Score', 'Temperament'];const genericDataFuncs = [() => Math.ceil(Math.random() * 100),sillyName,sillyWord,() => Math.floor(Math.random() * 10_000) / 100,() => randomFrom(['Goofy', 'Wacky', 'Silly', 'Funny', 'Serious']),];+const fancyCellRenderers = [+ function IdCell({ value }) {+ return <>{value}</>;+ },+ function NameCell({ value }) {+ return <strong>{value}</strong>;+ },+ function WordCell({ value }) {+ return <span style={{ fontStyle: 'italic' }}>{value}</span>+ },+ function ScoreCell({ value }) {+ const color = value < 50 ? 'pink' : 'aquamarine';+ return <span style={{ color }}>{value}</span>;+ },+ function TemperamentCell({value}) {+ return <>{value}</>;+ },+];// react-table expects memoized columns and data, so we export a React hook to permit doing that.export default function useData(numRows = 20, numCols = 5) {const columns = React.useMemo(() => (Array(numCols).fill(0).map((_, h) => {const name = genericHeaders[h % genericHeaders.length];const id = `col${h}`;return {Header: name,+ Cell: fancyCellRenderers[h % fancyCellRenderers.length],accessor: id};})), [numCols]);const data = React.useMemo(() => (Array(numRows).fill(0).map(() => {const row = {};for (let c = 0; c < numCols; c++) {row[`col${c}`] = genericDataFuncs[c % genericDataFuncs.length]();}return row;})), [numRows, numCols]);return { columns, data };}
With this feature, our demo is largely complete (click to interact):
We now have a table that can do the following:
There's an elephant in the room that we didn't address: dynamic data.
When there are too many rows to render at once, and when the rows change rapidly, how do you sort correctly?
How do you filter correctly? How do you prevent CPU and memory usage from exploding (spoiler: react-table
wasn't designed for this)?
We'll explore this and more in the next post.
Terms of Service|Privacy Policy
We are a Cloud Native Computing Foundation sandbox project.
Pixie was originally created and contributed by New Relic, Inc.
Copyright © 2018 - The Pixie Authors. All Rights Reserved. | Content distributed under CC BY 4.0.
The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage Page.
Pixie was originally created and contributed by New Relic, Inc.