If you’re building internal tools—admin panels, dashboards, CRMs, you name it—chances are you’re going to have to build a table component to display, edit, and manipulate data. But if you’re working in React, you (thankfully) don’t need to build it from scratch. The TanStack Table library gives you Hooks to get powerful tables up and running quickly.
By the end of this tutorial, you’ll know how to:
- Build a simple React table using TanStack Table, being able to modify data, columns, and headers.
- Give your React table a custom UI by passing CSS into each component using styled-components or piggybacking off a React component library.
- Extend your React table with features like sorting, filtering, and pagination.
Doing this from scratch could be a daunting challenge. Thankfully, the TanStack table library makes it dramatically easier.
TanStack Table (previously known as react-table) is an open-source library for building tables. Previously, it was a React-specific library; it still binds well with React, but now has an agnostic core and is also strongly compatible with Vue and Solid. TanStack Table is also now part of a grander TanStack suite of components, but is completely modular (by importing TanStack Table, you are not importing the rest of TanStack).
The library has over 24.1k stars on Github and is actively used by many big-name tech companies such as Google, Apple, and Microsoft. In fact, we like it so much here at Retool that we sponsor it!
Some of the benefits of TanStack Table is that it’s easy to set up, customize, and extend. The library covers the basics of a useful table—sorting, filtering, and pagination—but also goes much deeper with advanced features like:
- Grouping
- Expanded State
- Custom Plugin Hooks
Notably, TanStack Table is a “headless” UI library. The library doesn’t actually render a user interface. This un-opinionated design gives you more control over the look and feel of the TanStack Table component. If you’re feeling like that makes TanStack Table vaporware, don’t worry; we’ll delve more into the particulars soon.
Let’s imagine that we are building a table to display order information for customer service reps. Our table will need to display customer info (name and address) and order info (order number and date) for each customer’s purchase.
In this tutorial, we’ll accomplish that with TanStack Table, including making it sortable, filterable, and paginated. Our final goal is to have something like the following:
It could look better, but that’s what CSS is for!
First, we’ll build a basic table to display data, no styling or extra features. Our customer support reps need an easy way to view order information for each customer. Our simple table will have two top-level headers: User Info and Order Info. Under User Info, we need two secondary headers to display each customer’s Name and Address. Under Order Info, we need two more secondary headers to display the Date that the order was made and the Order Number.
In this section, we’ll build a table with four columns split into two groups. We’ll break down how to define the shape of column objects and data, parse header groups, and fill out our rows and cells. At the end, expect to see something like this:
1npm install @tanstack/react-table
Then, once TanStack Table is installed and imported, it’s time to define our data and columns by way of the useTable
Hook. TanStack table leverages Hooks, which came out in 2019 with version 16.8. If you haven't worked with Hooks before, we recommend taking a look at React’s documentation.
The most important Hook for our table is useTable
. We’ll pass two arguments to useTable
:
- data = table data defined with the
useMemo
Hook (data must be memo-ized before it can be passed touseTable
to cut down on calculation time by preventing unchanged data from being rerun)
1const data = React.useMemo(() => [{
2 name: 'Kim Parrish',
3 address: '4420 Valley Street, Garnerville, NY 10923',
4 date: '07/11/2020',
5 order: '87349585892118',
6 },
7 {
8 name: 'Michele Castillo',
9 address: '637 Kyle Street, Fullerton, NE 68638',
10 date: '07/11/2020',
11 order: '58418278790810',
12 },
13 {
14 name: 'Eric Ferris',
15 address: '906 Hart Country Lane, Toccoa, GA 30577',
16 date: '07/10/2020',
17 order: '81534454080477',
18 },
19 {
20 name: 'Gloria Noble',
21 address: '2403 Edgewood Avenue, Fresno, CA 93721',
22 date: '07/09/2020',
23 order: '20452221703743',
24 },
25 {
26 name: 'Darren Daniels',
27 address: '882 Hide A Way Road, Anaktuvuk Pass, AK 99721',
28 date: '07/07/2020',
29 order: '22906126785176',
30 },
31 {
32 name: 'Ted McDonald',
33 address: '796 Bryan Avenue, Minneapolis, MN 55406',
34 date: '07/07/2020',
35 order: '87574505851064',
36 },
37 ],
38 []
39)
40
- columns = column definitions defined with the
useMemo
Hook (column defs must be memoized before they can be passed touseTable
)
1const columns = React.useMemo(
2 () => [{
3 Header: 'User Info',
4 columns: [{
5 Header: 'Name',
6 accessor: 'name',
7 },
8 {
9 Header: 'Address',
10 accessor: 'address',
11 },
12 ],
13 },
14 {
15 Header: 'Order Info',
16 columns: [{
17 Header: 'Date',
18 accessor: 'date',
19 },
20 {
21 Header: 'Order #',
22 accessor: 'order',
23 },
24 ],
25 },
26 ],
27 []
28)
29
Take a second to look at the relationship between data and columns. The accessor
in columns is the “key” in the data object. This is important to be able to access the right data for each column once we use useTable
.
Once we have defined data and columns, it’s time to implement our useTable
Hook. Pass data and columns into useTable
, which will return properties that we can extract to build our table UI.
1const {
2 getTableProps,
3 getTableBodyProps,
4 headerGroups,
5 rows,
6 prepareRow,
7} = useTable({ columns, data })
8
Now, we’ll use these extracted properties to build out our table via familiar JSX tags–<table>
, <thead>
, <tr>
, <th>
, and <tbody>
–and then fill in our properties from useTable
.
First, we need <table>
to wrap the rest of our code, and we need to pass the getTableProps()
function in to resolve any table props.
1<table {...getTableProps()}>
2 ...
3</table>
4
Things start to heat up a little bit when we start to build our headers. On a high level, all we are doing is creating our header rows using the column header names that we defined above. Before we jump into the code, let’s look at the rendered table to get a better idea:
Each circled section in the table above is a headerGroup, which is simply an object that contains an array of headers for that row. For this table, we will have two header rows: the header circled in red is the first headerGroup, and the header circled in blue is the second headerGroup.
To get the data we need to build these headers out of headerGroups, we will be using JavaScript’s map()
method. If you’re unfamiliar with it, take a second to read the docs.
First, we have our <thead>
tag, which is simple enough. Inside of that tag, we are going to use map()
to parse each headerGroup, creating a new row using the <tr>
and passing that headerGroup’s getHeaderGroupProps()
method in.
1{headerGroups.map(headerGroup => (
2 <tr {...headerGroup.getHeaderGroupProps()}>
3 ...
4 </tr>
5))}
Inside of the <tr>
, we use map()
again, but this time on the array of headers. Each header object has a Header
property (which is the name you’ll give each header), a render()
function, and another prop resolver function called getHeaderProps()
.
For each column, we use the <th>
tag to create the column, being sure to pass that column’s prop resolver function getHeaderProps()
and then use the render()
function to access the Header.
1<thead>
2 {headerGroups.map(headerGroup => (
3 <tr {...headerGroup.getHeaderGroupProps()}>
4 {headerGroup.headers.map(column => (
5 <th {...column.getHeaderProps()}>{column.render('Header')}</th>
6 ))}
7 </tr>
8 ))}
9</thead>
Similar to how we did <table>
and <thead>
, we add <tbody>
and pass the prop resolver function getTableBodyProps()
in. Then, we use map()
to iterate through rows, which is an array of Row objects. Each Row object has a cells field, which is just an array of Cell objects that contain the data for each cell in the row.
For each row, we need to pass the row object to the prepareRow()
function, which helps with rendering efficiently. Next, we return <tr>
tags to render the row. In each <tr>
, we again use map()
to parse cells. For each cell, we create a <td>
tag, pass in the prop resolver function getCellProps()
, and then render the cell data using the render()
method.
1<tbody {...getTableBodyProps()}>
2 {rows.map(row => {
3 prepareRow(row)
4 return (
5 <tr {...row.getRowProps()}>
6 {row.cells.map(cell => {
7 return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
8 })}
9 </tr>
10 )
11 })}
12</tbody>
Let’s put all of that together to render our table.
1return (
2 <table {...getTableProps()}>
3 <thead>
4 {headerGroups.map(headerGroup => (
5 <tr {...headerGroup.getHeaderGroupProps()}>
6 {headerGroup.headers.map(column => (
7 <th {...column.getHeaderProps()}>{column.render('Header')}</th>
8 ))}
9 </tr>
10 ))}
11 </thead>
12 <tbody {...getTableBodyProps()}>
13 {rows.map(row => {
14 prepareRow(row)
15 return (
16 <tr {...row.getRowProps()}>
17 {row.cells.map(cell => {
18 return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
19 })}
20 </tr>
21 )
22 })}
23 </tbody>
24 </table>
25)
Using the above code, you’ll end up with a rendered table that looks like this:
Even if you’re building a tool that will only be used by an internal team, it’s still important for the UI to look good (or at least not terrible). Styling (at least the basics) is extra important with TanStack Table too since no components are actually rendered as part of the library. Without any styling, you’ll end up with a table like this:
You can style your TanStack Table component by either creating custom styles or through a React component library. The final product from this section will look like this:
Isn’t it beautiful? Let's explore some options for making this table a reality.
Styling your table with react-table is as simple as passing CSS into the style
prop of each component. Let’s look at the <th>
tag for styling a header row:
1<th
2 {...column.getHeaderProps()}
3 style={{
4 borderBottom: 'solid 3px blue',
5 background: 'green',
6 color: 'white',
7 fontWeight: 'bold',
8 }}
9>
You can also use CSS files and CSS modules if you’d like (see React’s CSS docs for more info).
styled-components
is a nifty React library that lets you style React components with CSS directly inside of JS code (as opposed to external CSS files). Lately, it’s become a really popular way to handle component styling in React, so you might want to use it for your table.
To use styled-components
, install the library and import it into your project. Create a component Styles
that uses styled
from the styled-components
library to create a div
with the styles for your table in CSS. Move all of your code for creating the Table
component into its own function. Then, in your App
function (where your columns and data are defined), return <Styles>
with your <Table>
rendered inside. This will apply the styles from the styled-components
on to your table.
1import styled from 'styled-components'
2
3/* Pssst this has the rest of the styling we've been using
4to give our table borders and non-ugly spacing */
5
6const Styles = styled.div`
7 table {
8 border-spacing: 0;
9 border: 1px solid black;
10
11 tr {
12 :last-child {
13 td {
14 border-bottom: 0;
15 }
16 }
17 }
18
19 th,
20 td {
21 padding: 0.5rem;
22 border-bottom: 1px solid black;
23 border-right: 1px solid black;
24
25 :last-child {
26 border-right: 0;
27 }
28 }
29
30 th {
31 background: green;
32 border-bottom: 3px solid blue;
33 color: white;
34 fontWeight: bold;
35 }
36 }
37`
38
39function Table({ columns, data }) {
40 const {
41 getTableProps,
42 getTableBodyProps,
43 headerGroups,
44 rows,
45 prepareRow,
46 } = useTable({
47 columns,
48 data,
49 })
50
51 // Render the UI for your table
52 return (
53 <table {...getTableProps()} >
54 ...
55 </table>
56 )
57}
58
59function App() {
60 const columns = React.useMemo(...)
61
62 const data = React.useMemo(...)
63
64 return (
65 <Styles>
66 <Table columns={columns} data={data} />
67 </Styles>
68 )
69}
70
71export default App
If you don’t want to style things yourself, using a React component library is the way to go. For this example, we’re going to use the material-ui
library to create a nice table with TanStack Table.
Going off of the Cell styling example above, we simply have to import TableCell
from @material-ui/core/TableCell
.
1import TableCell from '@material-ui/core/TableCell'
2...
3<TableCell {...cell.getCellProps()}>
4 {cell.render('Cell')}
5</TableCell>
6
This will bring all of the styling of the TableCell
component in material-ui
. No extra work for you!
No table worth rendering is going to have only two columns and three rows, like our example. Most likely, you're going to have a hefty portion of columns and row upon row of data. You'll need features to allow users to sift through all that data, like sorting, filtering, and pagination.
We want to give our customer service reps the ability to easily find what they are looking for, and sorting is a great way to accomplish that! If the reps want to see the most recent orders placed, they can sort by date from the Date column. If they want to scan through the customers alphabetically, they can sort by name in the Name column.
Sorting is accomplished by using the useSortBy
Hook from TanStack Table. Be sure to add that to your import statements:
1import { useTable, useSortBy } from 'react-table'
2
Next, you’ll need to pass useSortBy
into the useTable
Hook arguments:
1const {
2 getTableProps,
3 headerGroups,
4 rows,
5 getRowProps,
6 prepareRow,
7} = useTable(
8 {
9 columns,
10 data,
11 },
12 useSortBy,
13)
14
If you want the sorting to be done by anything other than the default alphanumeric value, you’ll need to update your columns
definition with a sortType
field. Sorting options include:
alphanumeric
: best for sorting letters and numbers (default).basic
: best for sorting numbers between 0 and 1.datetime
: best for sorting by date.
For this example, we will be using the default, but if you needed to add that code, it would look like this:
1const columns = React.useMemo(
2 () => [
3 {
4 Header: 'Rating',
5 accessor: 'rating',
6 sortType: 'basic',
7 },
8 ],
9 []
10)
11
Now there are two more things to do. First, pass the getSortByToggleProps()
function into your getHeaderProps()
function. The getSortByToggleProps()
function resolves props for toggling the sort direction when the user clicks on the header.
Second, add a span
tag to display an arrow up, an arrow down, or nothing to the column header to indicate how that column is sorted. You can determine how the column is sorted by checking column properties isSorted
and isSortedDesc
.
1<thead>
2 {headerGroups.map(headerGroup => (
3 <tr {...headerGroup.getHeaderGroupProps()}>
4 {headerGroup.headers.map(column => (
5 <th {...column.getHeaderProps(column.getSortByToggleProps())}>
6 {column.render('Header')}
7 <span>
8 {column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''}
9 </span>
10 </th>
11 ))}
12 </tr>
13 ))}
14</thead>
Check out this code sandbox for a more advanced version of sorting using TanStack Table.
For the sake of simplicity, this tutorial is going to focus on how to add a text filter on the columns of our simple table. This will give our customer support reps the ability to quickly find the information they are looking for. If a customer contacts them, the rep can easily search the Names column for that customer to find their orders or search the Order Numbers column to look up a specific order.
For further examples of all the different kinds of filters (including global, which is really useful), check out the TanStack Table docs.
Filtering is accomplished by using the useFilters()
Hook from react-table. Be sure to add that to your import statements:
1import { useTable, useFilters } from 'react-table'
2
Next, you’ll need to pass useFilters
into the useTable
Hook arguments:
1const {
2 getTableProps,
3 headerGroups,
4 rows,
5 getRowProps,
6 prepareRow,
7} = useTable(
8 {
9 columns,
10 data,
11 },
12 useFilters,
13)
14
Now, we are going to add the UI for the column filter into our table. If there is a filter applied to this column, we render the UI for the filter by calling the column’s render()
method on the Filter field. Otherwise, do nothing.
1<th {...column.getHeaderProps()}>
2 {column.render('Header')}
3 <div>{column.canFilter ? column.render('Filter') : null}</div>
4</th>
5
But wait! We haven’t defined the UI for the filter yet. We’ll need a function to do that. For our filter function, we first want to find out how many rows are left to be filtered, so we can display that number to the user as an input placeholder. Then, we will render an <input>
for the user to type what they want to filter.
1function TextFilter({
2 column: { filterValue, preFilteredRows, setFilter },
3}) {
4 const count = preFilteredRows.length
5
6 return (
7 <input
8 value={filterValue || ''}
9 onChange={e => {
10 setFilter(e.target.value || undefined)
11 }}
12 placeholder={`Search ${count} records...`}
13 />
14 )
15}
16
Our TextFilter()
function receives three values from column:
- filterValue: the current value that this column is using to filter.
- This value is set from the table’s state filters object.
- preFilteredRows: an array of rows passed to the column before any filtering was done.
- setFilter: the function that takes in a filterValue in order to update the filterValue of this column (in this case, it’s taking the value that the user types into the
<input>
).
Once we’ve defined our filter function, we’ll update the definition of our column object to have a Filter
field. Add this code to your Table function:
1const defaultColumn = React.useMemo(
2 () => ({
3 Filter: TextFilter,
4 }),
5 []
6)
7
Finally, make sure to pass defaultColumn
in with the columns and data arguments when you use useTable()
:
1const {
2 getTableProps,
3 ...
4} = useTable(
5 {
6 columns,
7 data,
8 defaultColumn,
9 },
10 useFilters,
11)
12
The data we created for the example in this tutorial is pretty small compared to what you would see in the real world. Only six rows? Please. In reality, our customer support reps would be dealing with hundreds (maybe even thousands) of rows of customer data. To avoid long render times and ginormous pages to scroll through, we are going to add pagination to our table. This will allow react-table to only deal with rendering some rows at a time and will take some strain off the customer support reps from having to look at overwhelming amounts of data.
Pagination is accomplished by using the usePagination()
Hook from TanStack Table. Be sure to add that to your import statements:
1import { useTable, usePagination } from 'react-table'
2
Next, you’ll need to pass usePagination()
into the useTable()
Hook arguments, set the initial state (if you want to start the pageIndex
at anything other than 0 or have the pageSize
bigger or smaller than 10), and extract extra properties from what it returns.
1const {
2 getTableProps,
3 headerGroups,
4 getRowProps,
5 prepareRow,
6 page,
7 pageOptions,
8 state: { pageIndex, pageSize },
9 previousPage,
10 nextPage,
11 canPreviousPage,
12 canNextPage,
13 } = useTable(
14 {
15 columns,
16 data,
17 initialState: { pageSize: 2 },
18 },
19 usePagination,
20 )
21
Note that as well as iterating through rows
in <tbody>
as we did previously before pagination, we are going to iterate through page
, which is similar to rows
except it only has the number of rows that fit on the page. If you don’t do this, you can click those buttons as much as you want—the data won’t move. Trust me.
1<tbody {...getTableBodyProps()}>
2 {page.map(row => {
3 prepareRow(row)
4 ...
5
In this example, we have a button to go to the Previous Page, a button to go to the Next Page, and an input that lets the user type a page number to jump to.
1return (
2 <div>
3 <table {...getTableProps()}>
4 ...
5 </table>
6 <div>
7 <button onClick={() => previousPage()} disabled={!canPreviousPage}>
8 Previous Page
9 </button>
10 <button onClick={() => nextPage()} disabled={!canNextPage}>
11 Next Page
12 </button>
13 <div>
14 Page{' '}
15 <em>
16 {pageIndex + 1} of {pageOptions.length}
17 </em>
18 </div>
19 </div>
20 </div>
21 )
22}
23
Check out this code sandbox for a more advanced version of pagination using @tanstack/react-table
.
Hopefully, this tutorial helps you understand how to create, style, and extend a table in React using TanStack Table. If you want to dig in deeper, we recommend checking out this suite of examples from the docs. It has fully fleshed-out examples of most things that TanStack Table has to offer, from pagination and sorting to filtering, grouping, expandable rows, and row selection.
Retool provides you with pre-built React components like the React table for quickly building apps right out of the box. Check out the Retool’s Component library or Docs for more info, or start building for free on Retool today.
Reader