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 Creating Tables in React

React table v7 is a lightweight (5-14kb), headless (100% customizable), and fully controllable tool for building fast and extendable data grids for React. The Library was created by Tanner Linsley, and it is clearly documented on Github.

Differences and Migration from v6 to v7

It’s true that there is a clear and significant difference between the two versions. Version 6 of React Table requires importing the main component and ready-made CSS styles. Both the functionality and the 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. The responsibility for providing and rendering table markup rests solely with us. 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. It’s up to us to decide how to build and what functionalities to use. The provided collection of custom React Hooks brings us one step closer to achieving our goal.

You may have already used React Table v6 in the past. This version enjoys great popularity; that said, as the creator himself has pointed out, it could no longer be maintained. Perhaps this very fact or the 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. With each subsequent step, we equip 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 begin then!

Project Setup

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

$ npx create-react-app table-example

The next step is to 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

Our first task is to fetch 100 user contacts. We’ll do this with native JavaScript fetch and the 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

Once we have our data all ready, the next step is to define the column structure (an array of objects that consists of header - column name and accessor - key in data). For optimization purposes, we wrap our columns in the 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 destructre, and we need those to build our table. Their purpose and place are explained 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 found here. Don’t forget to star the repo if you find it useful!

Add Bootstrap Table Style

As we mentioned earlier, version 7 of React Table does not support any UI view. The style of the table is up to us. In our example, we utilize the Bootstrap appearance for this purpose. 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, all we need to do is replace the HTML table with the Bootstrap Table component.

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

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

The code with added bootstrap is available here

Custom Cell

React Table 7 allows you to define a custom look for each cell. We can do this in the definition for a given column. As far as Table Cell goes, 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, which we use to render the hemisphere sign based on user coordinates. In accessor, we will destructure the latitude and longitude values to determine the user’s hemisphere.

{
        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;
        }
},

Next, we render the 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 the 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 the 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
)

All we need to do is to make a minor change in the way we render column headers. In the column header definition, we invoke the function getSortByToggleProps on a column - which returns an onClick event responsible for changing the sorting direction. We have included generateSortingIndicator - a helper function which returns the sort 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, we can do sorting on each column by default. If we want to disable sorting in a particular column, we need to set the disableSortBy property to true in the column definition.

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

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

Filtering - useFilters Hook

filtering

The next feature that we will add to our table is column filtering. With the useFilters hook, we can do it an easy and accessible way. Like before, we need to pass the useFilters hook as a parameter to our main useTable hook. useFilters must be placed before useSortBy, otherwise 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, the next step will be to modify the way of rendering for our <th>. We add the component that will display the view for our filters. On top of that, 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 filter DefaultColumnFilter - will render Text Input, which filters based on the entered text (the 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 the “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

The code for the table with added filters is available 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 the 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 the 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>

We use the App.js file to 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>
        )
      },

The renderRowSubComponent function, renders a 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 sub-components functionality after clicking the right emoji.

The code can be found here.

Pagination - usePagingation Hook

pagination

The last hook we are going to implement is usePagination. 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, we define initialState 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 only keeps rows for an active page),

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

Let’s create two helper 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. This is only an example - you can build and style it in any way 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 now have a 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 is equipped with some interesting features. The table made in this article is merely an introduction to building more advanced, custom tables with the help of React Table 7!

In fact, there is a whole another spectrum of functionalities that we can get with React Table 7. It’s up to us how we will construct our table. For more information and examples please refer to this great documentation.


Written by Bartek Bajda.