If you’re building internal tools – admin panels, dashboards, CRMs, you name it – chances are you’re thinking about how to build a table component to display, edit, and manipulate data. And if you’re working in React, you (thankfully) don’t need to build it from scratch: the react-table library gives you Hooks to get tables up and running quickly.

By the end of this tutorial, you’ll know how to:

  • Build a simple table with React and modify data, columns, and headers
  • Give your table a custom UI by passing CSS into each component using styled-components or piggybacking off a React component library.
  • Extend your table with more features like sorting, filtering, and pagination.

Learning all of these things yourself can be complicated. Thankfully, the react-table library is killer (and we made you this guide to help).

Intro: react-table


React-table is an open-source library specifically for building (you guessed it) tables in React. The library has over 11.5k stars on GitHub and is used by tons of big-name tech companies, like Google, Apple, and Microsoft. Plus, we like it so much here at Retool that we sponsor it.

We like react-table because 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

It should be noted that react-table is a “headless” UI library. The library doesn’t actually render a user interface. While this may sound strange, it was designed this way to give you more control over the look and feel of the react-table component while keeping the package size small. Don’t worry, adding UI is easy, and we will cover that later on.

Since we’re all about internal tools, 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.

When you’ve finished working through this tutorial, you will have five versions of a table built with react-table: simple, styled, sortable, filterable, and paged. Below is the final, paged version we’re aiming for.

Retool-React-Table-Pagination

It could look better, but that’s what CSS is for!

Build a simple table with react-table


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:

Retool-React-Table
Note that we did add a tiny bit of extra styling to this section, so the table has lines.

Taking care of housekeeping basics first, you’ll need to install react-table by using a package manager (Yarn or npm) and import the library into your React app:

import { useTable } from 'react-table';

Then, once react-table is installed and imported, it’s time to define our data and columns by way of the useTable Hook. React-table leverages Hooks, which are a fairly new addition to React (as of version 16.8). If you’re not familiar with React Hooks, we recommend taking a look at React’s Hooks at a Glance documentation.

The most important Hook for our table is useTable. We’ll pass two arguments to useTable:

  1. data = table data defined with the useMemo Hook (data must be memo-ized before it can be passed to useTable to cut down on calculation time by preventing unchanged data from being rerun)
const data = React.useMemo(() =>
 [
 {
 name: 'Kim Parrish',
 address: '4420 Valley Street, Garnerville, NY 10923',
 date: '07/11/2020',
 order: '87349585892118',
 },
 {
 name: 'Michele Castillo',
 address: '637 Kyle Street, Fullerton, NE 68638',
 date: '07/11/2020',
 order: '58418278790810',
 },
 {
 name: 'Eric Ferris',
 address: '906 Hart Country Lane, Toccoa, GA 30577',
 date: '07/10/2020',
 order: '81534454080477',
 },
 {
 name: 'Gloria Noble',
 address: '2403 Edgewood Avenue, Fresno, CA 93721',
 date: '07/09/2020',
 order: '20452221703743',
 },
 {
 name: 'Darren Daniels',
 address: '882 Hide A Way Road, Anaktuvuk Pass, AK 99721',
 date: '07/07/2020',
 order: '22906126785176',
 },
 {
 name: 'Ted McDonald',
 address: '796 Bryan Avenue, Minneapolis, MN 55406',
 date: '07/07/2020',
 order: '87574505851064',
 },
 ],
 []
)
  1. columns = column definitions defined with the useMemo Hook (column defs must be memoized before they can be passed to useTable)
const columns = React.useMemo(
 () => [
 {
 Header: 'User Info',
 columns: [
 {
 Header: 'Name',
 accessor: 'name',
 },
 {
 Header: 'Address',
 accessor: 'address',
 },
 ],
 },
 {
 Header: 'Order Info',
 columns: [
 {
 Header: 'Date',
 accessor: 'date',
 },
 {
 Header: 'Order #',
 accessor: 'order',
 },
 ],
 },
 ],
 []
)

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.

const {
 getTableProps,
 getTableBodyProps,
 headerGroups,
 rows,
 prepareRow,
} = useTable({ columns, data })

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.

Table

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.

<table {...getTableProps()}>
 ...
</table>

Headers

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:

Retool_headers_groups

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.

{headerGroups.map(headerGroup => (
   <tr {...headerGroup.getHeaderGroupProps()}>
     ...
   </tr>
))}

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.

<thead>
 {headerGroups.map(headerGroup => (
   <tr {...headerGroup.getHeaderGroupProps()}>
     {headerGroup.headers.map(column => (
       <th {...column.getHeaderProps()}>{column.render('Header')}</th>
     ))}
   </tr>
 ))}
</thead>

Table Body

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.

Retool-React-Table-Component-Table-Body

The orange circle shows a row, and the pink circle shows a cell.

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.

<tbody {...getTableBodyProps()}>
 {rows.map(row => {
   prepareRow(row)
   return (
     <tr {...row.getRowProps()}>
       {row.cells.map(cell => {
         return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
       })}
     </tr>
   )
 })}
</tbody>

Let’s put all of that together to render our table.

return (
 <table {...getTableProps()}>
   <thead>
     {headerGroups.map(headerGroup => (
       <tr {...headerGroup.getHeaderGroupProps()}>
         {headerGroup.headers.map(column => (
           <th {...column.getHeaderProps()}>{column.render('Header')}</th>
         ))}
       </tr>
     ))}
   </thead>
   <tbody {...getTableBodyProps()}>
     {rows.map(row => {
       prepareRow(row)
       return (
         <tr {...row.getRowProps()}>
           {row.cells.map(cell => {
             return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
           })}
         </tr>
       )
     })}
   </tbody>
 </table>
)

Using the above code, you’ll end up with a rendered table that looks like this:

Retool-Simple-React-Table-Final

Give your table a custom UI


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 react-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:

Retool-Simple-Unstyled-React-Table

You can style your react-table component by either creating custom styles or through a React component library. The final product from this section will look like this:

Retool-React-Table-Styled

Isn’t it beautiful?

Using the style Prop

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:

<th
 {...column.getHeaderProps()}
 style={{
   borderBottom: 'solid 3px blue',
   background: 'green',
   color: 'white',
   fontWeight: 'bold',
 }}
>

You can also use CSS files and CSS modules if you’d like. Check out React’s CSS docs for more info.

Using styled-components

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.

import styled from 'styled-components'

/* Pssst this has the rest of the styling we've been using
to give our table borders and non-ugly spacing */

const Styles = styled.div`
 table {
   border-spacing: 0;
   border: 1px solid black;

   tr {
     :last-child {
       td {
         border-bottom: 0;
       }
     }
   }

   th,
   td {
     padding: 0.5rem;
     border-bottom: 1px solid black;
     border-right: 1px solid black;

     :last-child {
       border-right: 0;
     }
   }
  
   th {
     background: green;
     border-bottom: 3px solid blue;
     color: white;
     fontWeight: bold;
   }
 }
`

function Table({ columns, data }) {
 const {
   getTableProps,
   getTableBodyProps,
   headerGroups,
   rows,
   prepareRow,
 } = useTable({
   columns,
   data,
 })

 // Render the UI for your table
 return (
   <table {...getTableProps()} >
     ...
   </table>
 )
}

function App() {
 const columns = React.useMemo(...)

 const data = React.useMemo(...)

 return (
   <Styles>
     <Table columns={columns} data={data} />
   </Styles>
 )
}

export default App

Using a React Component Library

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 react-table.

Going off of the Cell styling example above, we simply have to import TableCell from @material-ui/core/TableCell.

import TableCell from '@material-ui/core/TableCell'
...
<TableCell {...cell.getCellProps()}>
   {cell.render('Cell')}
</TableCell>

This will bring all of the styling of the TableCell component in material-ui. No extra work for you!

Click here for a full sandbox version of this code.

Extend your table with more features


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.

Sorting

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 react-table. Be sure to add that to your import statements:

import { useTable, useSortBy } from 'react-table' 

Next, you’ll need to pass useSortBy into the useTable Hook arguments:

const {
 getTableProps,
 headerGroups,
 rows,
 getRowProps,
 prepareRow,
} = useTable(
 {
   columns,
   data,
 },
 useSortBy,
)

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:

const columns = React.useMemo(
 () => [
   {
     Header: 'Rating',
     accessor: 'rating',
     sortType: 'basic',
   },
 ],
 []
)

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.

<thead>
 {headerGroups.map(headerGroup => (
   <tr {...headerGroup.getHeaderGroupProps()}>
     {headerGroup.headers.map(column => (
       <th {...column.getHeaderProps(column.getSortByToggleProps())}>
         {column.render('Header')}
         <span>
           {column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''}
         </span>
       </th>
     ))}
   </tr>
 ))}
</thead>

Retool-Styling-React-Table

Check out this code sandbox for a more advanced version of sorting using react-table.

Filtering

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 this the react-table docs.

Filtering is accomplished by using the useFilters() Hook from react-table. Be sure to add that to your import statements:

import { useTable, useFilters } from 'react-table' 

Next, you’ll need to pass useFilters into the useTable Hook arguments:

const {
 getTableProps,
 headerGroups,
 rows,
 getRowProps,
 prepareRow,
} = useTable(
 {
   columns,
   data,
 },
 useFilters,
)

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.

<th {...column.getHeaderProps()}>
 {column.render('Header')}
 <div>{column.canFilter ? column.render('Filter') : null}</div>
</th>

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.

function TextFilter({
 column: { filterValue, preFilteredRows, setFilter },
}) {
 const count = preFilteredRows.length

 return (
   <input
     value={filterValue || ''}
     onChange={e => {
       setFilter(e.target.value || undefined)
     }}
     placeholder={`Search ${count} records...`}
   />
 )
}

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 = Array of rows passed to the column before any filtering was done.
  • setFilter = 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:

const defaultColumn = React.useMemo(
 () => ({
   Filter: TextFilter,
 }),
 []
)

Finally, make sure to pass defaultColumn in with the columns and data arguments when you use useTable():

const {
 getTableProps,
 ...
} = useTable(
 {
   columns,
   data,
   defaultColumn,
 },
 useFilters,
)

Retool-React-Table-Filtering

Pagination

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 react-table. Be sure to add that to your import statements:

import { useTable, usePagination } from 'react-table' 

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.

const {
   getTableProps,
   headerGroups,
   getRowProps,
   prepareRow,
   page,
   pageOptions,
   state: { pageIndex, pageSize },
   previousPage,
   nextPage,
   canPreviousPage,
   canNextPage,
 } = useTable(
   {
     columns,
     data,
     initialState: { pageSize: 2 },
   },
   usePagination,
 )

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.

<tbody {...getTableBodyProps()}>
 {page.map(row => {
   prepareRow(row)
   ...

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.

return (
   <div>
     <table {...getTableProps()}>
       ...
     </table>
     <div>
       <button onClick={() => previousPage()} disabled={!canPreviousPage}>
         Previous Page
       </button>
       <button onClick={() => nextPage()} disabled={!canNextPage}>
         Next Page
       </button>
       <div>
         Page{' '}
         <em>
           {pageIndex + 1} of {pageOptions.length}
         </em>
       </div>
     </div>
   </div>
 )
}

Retool-React-Table-Pagination

Check out this code sandbox for a more advanced version of pagination using react-table.

Your Table Component with react-table


Hopefully, this tutorial helps you understand how to create, style, and extend a table in React using react-table. For more advanced tutorials, we recommend checking out the “Kitchen Sink” example from react-table’s docs. It has fully fleshed-out examples of most things that react-table has to offer, from pagination and sorting to filtering, grouping, expandable rows, and row selection.