The Widlarz Group Blog

React Table 7 - Hooks based library

May 15, 2020

react

react table 7

hooks

bootstrap

React Table 7 - hooks approach to create tables in React

React table v7 is a lightweight (5-14kb), headless (100% customizable), and fully controllable tool for building fast and extendable datagrids for React. The Library was created by Tanner Linsley, and has a great documentation on Github.

Differences and migration from v6 to v7

There is indeed a significant difference between the two versions. When it comes to React Table in version 6, you need to import the main component and ready-made CSS styles. Both functionality and appearance of the UI are controlled by passing the appropriate props to the main component. The latest version of React Table (v7) is headless, which means that it doesn’t provide or support any UI elements. We ourselves are responsible for providing and rendering table markup. This gives us the opportunity to build a unique look and feel for our table. React Table v7 uses React Hooks both internally and externally for almost everything. We decide how to build and what functionalities we want to use. The provided collection of custom React Hooks help us achieve our goal.

You may have used React Table v6. This version is very popular, however, as the creator pointed out, it would no longer be maintained. Perhaps this or its architecture based on hooks and thus its performance, will prompt you to switch to a newer version.

Starting with React Table 7

The best approach for starting with React Table v7 is learning by building a simple table, and then expanding it with new functionalities.

In this article, we start by building a simple table from scratch together. With each subsequent step, we enrich our table with new features such as sorting, filtering, sub-components, pagination and we add bootstrap styling as well.

Below is a photo of the final version we will build together, step by step. You can play with the table here: Demo.

photo-1



At the end of each step, there’s a link to the current code. Let’s start then!

Project Setup

We start by creating a new React project using create-react-app

$ npx create-react-app table-example

Then install the react-table library

$ npm install react-table
or
$ yarn add react-table

Prepare Data

We use radnomuser API as data to fill the table API docs

Our first task is to fetch 100 user contacts. We’ll do it with native Javascript fetch and React useEffect hook.

Each contact is a plain JS object that consists of information such as name, location, gender, contact details and picture.

import React, { useEffect, useState } from "react"

const App = () => {
  const [data, setData] = useState([])
  useEffect(() => {
    const doFetch = async () => {
      const response = await fetch("https://randomuser.me/api/?results=100")
      const body = await response.json()
      const contacts = body.results
      console.log(contacts)
      setData(contacts)
    }
    doFetch()
  }, [])

  return <div>Hello</div>
}

Define Columns

When we have our data ready, the next step is to define columns structure (an array of objects that consists of Header - column name and accessor - key in data). For optimization purposes, we wrap our columns in React useMemo hook.

const columns = useMemo(
  () => [
    {
      Header: "Title",
      accessor: "name.title",
    },
    {
      Header: "First Name",
      accessor: "name.first",
    },
    {
      Header: "Last Name",
      accessor: "name.last",
    },
    {
      Header: "Email",
      accessor: "email",
    },
    {
      Header: "City",
      accessor: "location.city",
    },
  ],
  []
)

data-1

Table Rendering - useTable hook

The first and most important hook we’ll use is useTable

useTable requires an object with two properties: data (our contacts) and columns, which we have previously defined. The hook returns properties that we need to destruct. We need them to build our table and their purpose and place are in the code below.

We start building our Table in a separate component TableContainer.js This is the basic version of the table, which will be the base for implementing additional functionalities. Please note how our destructured properties from the useTable hook are used.

// App.js
import TableContainer from "./TableContainer"

return <TableContainer columns={columns} data={data} />
// TableContainer.js
import React from "react"
import { useTable } from "react-table"

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

  return (
    // If you're curious what props we get as a result of calling our getter functions (getTableProps(), getRowProps())
    // Feel free to use console.log()  This will help you better understand how react table works underhood.
    <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>
  )
}

export default TableContainer

All of the code we have written so far, can be seen here. Don’t forget to star the repo if you find it useful!

Add Bootstrap Table Style

As we mentioned earlier, React Table in version 7 does not support any UI view, it depends on us how the table looks like. In our example, we utilize the Bootstrap appearance for the Table. To do this, we need to install two packages.

$ yarn add bootstrap
$ yarn add reactstrap

The first thing to do is to import bootstrap styles. A bootstrap container is also added in order to position the table into the center.

// App.js
import { Container } from "reactstrap"
import "bootstrap/dist/css/bootstrap.min.css"

// return <TableContainer columns={columns} data={data} />;
return (
  <Container style={{ marginTop: 100 }}>
    <TableContainer columns={columns} data={data} />
  </Container>
)

After importing bootstrap styles, replacing the HTML table with the Bootstrap Table component is all we have to do now.

// TableContainer.js
import { Table } from 'reactstrap';

//  <table {...getTableProps()}>
<Table bordered hover {...getTableProps()}>

code with added bootstrap here

Custom Cell

React Table 7 allows you to define custom look for each cell. We can do this in the definition for a given column. As far as Table Cell, we can render any React Component.

      {
        Header: 'Color',
        accessor: 'color'
         // Cell has access to row values. If you are curious what is inside cellProps, you can  console log it
        Cell: (cellProps) => {
          return <YourReactComponent {...cellProps}/>
        }
      }

In our example we are going to create a new column - Hemisphere, in which we render hemisphere sign based on user coordinates. In accessor, we will destructure values for latitude and longitude to determine which hemisphere the user is on.

{
        Header: 'Hemisphere',
        accessor: (values) => {
          const { latitude, longitude } = values.location.coordinates;
          const first = Number(latitude) > 0 ? 'N' : 'S';
          const second = Number(longitude) > 0 ? 'E' : 'W';
          return first + '/' + second;
        }
},

then we render a respective sign.

{
        Header: 'Hemisphere',
        accessor: (values) => {
          const { latitude, longitude } = values.location.coordinates;
          const first = Number(latitude) > 0 ? 'N' : 'S';
          const second = Number(longitude) > 0 ? 'E' : 'W';
          return first + '/' + second;
        },
        // we can also write code below as a separate React Component
        Cell: ({ cell }) => {
          const { value } = cell;

          const pickEmoji = (value) => {
            let first = value[0]; // N or S
            let second = value[2]; // E or W
            const options = ['⇖', '⇗', '⇙', '⇘'];
            let num = first === 'N' ? 0 : 2;
            num = second === 'E' ? num + 1 : num;
            return options[num];
          };

          return (
            <div style={{ textAlign: 'center', fontSize: 18 }}>
              {pickEmoji(value)}
            </div>
          );
        }
},

Link to current code. Don’t forget to star the repo if you find it useful!

Sorting - useSortBy hook

sorting

React Table 7 allows us to easily create sorting for our table. To create sorting, we will need another hook from React Table hooks collection - useSortBy

We pass the useSortBy hook as a parameter to our main useTable hook. React Table automatically handles sorting in ascending/descending order.

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

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

The only thing we need to do is to make a small change in the way we render column headers. In column header definition, we invoke the function getSortByToggleProps on a column - which returns an onClick event responsible for changing sorting direction. We have written generateSortingIndicator - a helper function which returns sorting indicator based on sorting state (ascending/descending/no sorting). Code below.

// <th {...column.getHeaderProps()}>{column.render('Header')}</th>
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
  {column.render("Header")}
  {generateSortingIndicator(column)}
</th>
const generateSortingIndicator = column => {
  return column.isSorted ? (column.isSortedDesc ? " 🔽" : " 🔼") : ""
}

With this implementation, by default we can do sorting on each column. If we want to disable sorting in a particular column, we need to set disableSortBy property to true in a column definition.

      {
        Header: 'Title',
        accessor: 'name.title'
        disableSortBy: true
      },

You can find the code for table with sorting here. Again, don’t forget to star the repo if you find it useful and haven’t done it yet!

Filtering - useFilters hook

filtering

The next feature we will add to our table is column filtering. useFilters is a hook, which will enable us to do it in an easy and accessible way As previously, we need to pass useFilters hook as a parameter to our main useTable hook. We need to place useFilters before useSortBy. If we do not do this, React Table 7 will inform us about that fact in the console. (Error: React Table: The useSortBy plugin hook must be placed after the useFilters plugin hook!)

import { useTable, useSortBy, useFilters } from "react-table"

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

After connecting the useFilters hook, we are going to modify the way of rendering for our <th>. We add the component that will display the view for our filters. In addition, we wrap the rest of the header cell in <div> so that clicking on our filter does not trigger sorting in a column.

// <th {...column.getHeaderProps(column.getSortByToggleProps())}>
//   {column.render('Header')}
//   {generateSortingIndicator(column)}
<th {...column.getHeaderProps()}>
  <div {...column.getSortByToggleProps()}>
    {column.render("Header")}
    {generateSortingIndicator(column)}
  </div>
  <Filter column={column} />
</th>

Let’s create a new file filters.js, in which we are going to write views for our filters :)

Filter- a universal component for rendering a filter view

import React from "react"

export const Filter = ({ column }) => {
  return (
    <div style={{ marginTop: 5 }}>
      {column.canFilter && column.render("Filter")}
    </div>
  )
}

Now, we are going to write our two filters, which we want to use in our table. The first of them DefaultColumnFilter - will render Text Input, which filters based on the entered text (text filtering functionality is provided by default by the React Table). The second of them, SelectColumnFilter, renders Select Input which allows to choose from the available options.

import { Input, CustomInput } from "reactstrap"
export const DefaultColumnFilter = ({
  column: {
    filterValue,
    setFilter,
    preFilteredRows: { length },
  },
}) => {
  return (
    <Input
      value={filterValue || ""}
      onChange={e => {
        setFilter(e.target.value || undefined)
      }}
      placeholder={`search (${length}) ...`}
    />
  )
}
export const SelectColumnFilter = ({
  column: { filterValue, setFilter, preFilteredRows, id },
}) => {
  const options = React.useMemo(() => {
    const options = new Set()
    preFilteredRows.forEach(row => {
      options.add(row.values[id])
    })
    return [...options.values()]
  }, [id, preFilteredRows])

  return (
    <CustomInput
      id="custom-select"
      type="select"
      value={filterValue}
      onChange={e => {
        setFilter(e.target.value || undefined)
      }}
    >
      <option value="">All</option>
      {options.map(option => (
        <option key={option} value={option}>
          {option}
        </option>
      ))}
    </CustomInput>
  )
}

How to use our filters?

  1. DefaultColumnFilter - is passed as a default filter for columns to the useTable hook, which means that each of our column uses this filter until it is turned off or another filter is attached.
// TableContainer.js
import { Filter, DefaultColumnFilter } from './filters';

useTable(
    {
      columns,
      data
      defaultColumn: { Filter: DefaultColumnFilter }
    }
  );
  1. SelectColumnFilter - adding ‘Filter’ prop in Column Definition (overrides the default filter).
// App.js
import { SelectColumnFilter } from './filters';

      {
        Header: 'Title',
        accessor: 'name.title',
        Filter: SelectColumnFilter,
        filter: 'equals' // by default, filter: 'text', but in our case we don't want to filter options like text, we want to find exact match of selected option.
      },

If you don’t want to display any filter in a column, simply add this line of code in for that column:

disableFilters: true

Code for the table with added filters is here.

Sub Components - useExpanded hook

Sub Components

The first step is to import the useExpanded hook from React Table and join it with our useTable. Next a visibleColumns prop from useTable is destructured, to make sure that our Sub Component will take 100% of a table width.

import { useTable, useSortBy, useFilters, useExpanded } from 'react-table';

  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    visibleColumns,
  } = useTable(
    {
      columns,
      data,
      defaultColumn: { Filter: DefaultColumnFilter },
    },
    useFilters,
    useSortBy
    useSortBy,
    useExpanded
  );

To display the Sub Component, we need to modify the rendering for a table body <tr> We use the value row.isExpanded to determine whether we want to display Sub Component.

// <tr {...row.getRowProps()}>
//   {row.cells.map(cell => {
//     return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>;
//   })}
// </tr>
<Fragment key={row.getRowProps().key}>
  <tr>
    {row.cells.map(cell => {
      return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>
    })}
  </tr>
  {row.isExpanded && (
    <tr>
      <td colSpan={visibleColumns.length}>{renderRowSubComponent(row)}</td>
    </tr>
  )}
</Fragment>

App.js is the file in which we write the renderRowSubComponent function as well as the column definition with emoji indicating whether our row is open or not.

  // Add at the beginning of column definition
      {
        Header: () => null,
        id: 'expander', // 'id' is required
        Cell: ({ row }) => (
          <span {...row.getToggleRowExpandedProps()}>
            {row.isExpanded ? '👇' : '👉'}
          </span>
        )
      },

renderRowSubComponent - function, that render simple Bootstrap Card with contact details. We need to pass it as a prop to TableContainer.

// import { Container } from 'reactstrap';
import {
  Container,
  Card,
  CardImg,
  CardText,
  CardBody,
  CardTitle,
} from "reactstrap"

const renderRowSubComponent = row => {
  const {
    name: { first, last },
    location: { city, street, postcode },
    picture,
    cell,
  } = row.original
  return (
    <Card style={{ width: "18rem", margin: "0 auto" }}>
      <CardImg top src={picture.large} alt="Card image cap" />
      <CardBody>
        <CardTitle>
          <strong>{`${first} ${last}`} </strong>
        </CardTitle>
        <CardText>
          <strong>Phone</strong>: {cell} <br />
          <strong>Address:</strong> {`${street.name} ${street.number} - ${postcode} - ${city}`}
        </CardText>
      </CardBody>
    </Card>
  )
}
//  pass the function 'renderRowSubComponent' as a prop to our TableContainer

// <TableContainer columns={columns} data={data} />
<TableContainer
  columns={columns}
  data={data}
  renderRowSubComponent={renderRowSubComponent}
/>

After implementing the above steps, you should have a working subComponents functionality after clicking the right emoji.

Code can be found here.

Pagination - usePagingation hook

pagination

The last hook we are going to implement is the usePagination hook. As always, we need to add usePagination to our useTable. This hook requires destructuring of several additional props that we need to build our pagination. In addition, the initialState is defined in which we specify how many rows we want to display (pageSize) and from which page we start displaying (pageIndex).

import { useTable, useSortBy, useFilters, useExpanded, usePagination } from 'react-table';

 const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    // rows, -> we change 'rows' to 'page'
    page,
    prepareRow,
    visibleColumns
    // below new props related to 'usePagination' hook
    canPreviousPage,
    canNextPage,
    pageOptions,
    pageCount,
    gotoPage,
    nextPage,
    previousPage,
    setPageSize,
    state: { pageIndex, pageSize }
  } = useTable(
    {
      columns,
      data,
      defaultColumn: { Filter: DefaultColumnFilter }
      initialState: { pageIndex: 0, pageSize: 10 }
    },
    useFilters,
    useSortBy,
    useExpanded
    usePagination
  );

Instead of using rows, we use page (which keeps only rows for an active page),

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

Let’s create two helpers functions that can handle changing pages both in Text Input and Select Input

const onChangeInSelect = event => {
  setPageSize(Number(event.target.value))
}

const onChangeInInput = event => {
  const page = event.target.value ? Number(event.target.value) - 1 : 0
  gotoPage(page)
}

Then, below <Table> definition, we create our pagination. It is only an example. You can build and style it as you want!

// import { Table } from 'reactstrap';
import { Table, Row, Col, Button, Input, CustomInput } from "reactstrap"
<Fragment>
  <Table>{/* our table code here ... */}</Table>

  <Row style={{ maxWidth: 1000, margin: "0 auto", textAlign: "center" }}>
    <Col md={3}>
      <Button
        color="primary"
        onClick={() => gotoPage(0)}
        disabled={!canPreviousPage}
      >
        {"<<"}
      </Button>
      <Button
        color="primary"
        onClick={previousPage}
        disabled={!canPreviousPage}
      >
        {"<"}
      </Button>
    </Col>
    <Col md={2} style={{ marginTop: 7 }}>
      Page{" "}
      <strong>
        {pageIndex + 1} of {pageOptions.length}
      </strong>
    </Col>
    <Col md={2}>
      <Input
        type="number"
        min={1}
        style={{ width: 70 }}
        max={pageOptions.length}
        defaultValue={pageIndex + 1}
        onChange={onChangeInInput}
      />
    </Col>
    <Col md={2}>
      <CustomInput type="select" value={pageSize} onChange={onChangeInSelect}>
        >
        {[10, 20, 30, 40, 50].map(pageSize => (
          <option key={pageSize} value={pageSize}>
            Show {pageSize}
          </option>
        ))}
      </CustomInput>
    </Col>
    <Col md={3}>
      <Button color="primary" onClick={nextPage} disabled={!canNextPage}>
        {">"}
      </Button>
      <Button
        color="primary"
        onClick={() => gotoPage(pageCount - 1)}
        disabled={!canNextPage}
      >
        {">>"}
      </Button>
    </Col>
  </Row>
</Fragment>

If you have implemented the above steps, you should have working pagination for your table. usePagination is the last hook we have used to build our table.

You can find the whole code here.

Conclusions

Voilà, we have a ready table that has quite interesting features. The table made in this article is an introduction to building more advanced, custom tables with the help of React Table 7.

React Table 7 offers us many more possibilities than those we met in the article. It depends on us how we will construct our table. For more information and examples, I refer to very good documentation


Written by Bartek Bajda.