Link Search Menu Expand Document

Live stream with Taylor

Fetching and parsing the data

import { readRemoteFile } from "react-papaparse";

const [rawData, setRawData] = React.useState([]);

React.useEffect(() => {
  (async function fetchFunction() {
    readRemoteFile("/data/billboard.csv", {
      header: true,
      complete: raw => setRawData(raw.data)
    });
  })();
}, []);

console.log(rawData);

HTML structure of the table

<table>
  <tr>
    <th>Firstname</th>
    <th>Lastname</th>
    <th>Age</th>
  </tr>
  <tr>
    <td>Jill</td>
    <td>Smith</td>
    <td>50</td>
  </tr>
  <tr>
    <td>Eve</td>
    <td>Jackson</td>
    <td>94</td>
  </tr>
</table>

react-table

  • Collection of hooks for building powerful tables and datagrid experiences
  • It doesn't provide any markup or styles

memoize the data and column structure

  • Stop the table calculations happening every time there is a re-render.
const data = React.useMemo(() => rawData, [rawData]);
  • Need to make sure that it recalculates when the async data has been fetched.
const columns = React.useMemo(
  () => [
    {
      Header: "Track",
      accessor: "track"
    }
  ],
  []
);
  • There are other options for particular columns we will use later.
  • Header is the title of the column (Maths teacher anecdote -> columns hold up the roof)
  • accessor is the property of the row that needs to be accessed to get the value for the cell in question.
  • This can be nested "track.name" or even "track[0].name" if your data structure is more complex

Using the useTable hook

  • The two required props are data and columns which we've just prepared.
const table = useTable({ columns, data });

The docs suggest we destructure the elements we want from the returned object.

const {
  getTableProps, // resolves any props needs for the table wrapper
  getTableBodyProps, // resolves any props needed for the body wrapper
  headerGroups, // array of header groups
  rows, // an array of row objects
  prepareRow // a function that lazily prepares a row for rendering
} = useTable({
  columns,
  data
});

Now, let's use those elements to create our table.

<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, i) => {
      prepareRow(row);
      return (
        <tr {...row.getRowProps()}>
          {row.cells.map(cell => {
            return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
          })}
        </tr>
      );
    })}
  </tbody>
</table>

Add a new accessor and a specific column renderer

      {
        Header: "Album Art",
        accessor: "albumArtUrl",
        Cell: ({ value }) => <img src={value} />
      }

and

      {
        Header: "Play",
        accessor: 'trackPlayUrl',
        Cell: ({ value }) => <a href={value}>Listen on Spotify</a>
      }

Adding react-window to make things more responsive

  • Abstract a function to render the row
const RenderRow = React.useCallback(
  ({ index, style }) => {
    const row = rows[index];
    prepareRow(row);
    return (
      <div
        {...row.getRowProps({
          style
        })}
      >
        {row.cells.map(cell => {
          return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
        })}
      </div>
    );
  },
  [prepareRow, rows]
);
  • We're using useCallback as another memoizing level.

  • Replace the tbody with

<FixedSizeList height={800} itemCount={rows.length} itemSize={70}>
  {RenderRow}
</FixedSizeList>

A bit squashed, let's create a default column, pass to the table and then get back the total column widths.

const defaultColumn = React.useMemo(
  () => ({
    width: 350
  }),
  []
);
const {
  getTableProps, // resolves any props needs for the table wrapper
  getTableBodyProps, // resolves any props needed for the body wrapper
  headerGroups, // array of header groups
  rows, // an array of row objects
  prepareRow, // a function that lazily prepares a row for rendering
  totalColumnsWidth
} = useTable(
  {
    columns,
    data,
    defaultColumn
  },
  useBlockLayout
);

and update our fixed list

<FixedSizeList
  height={800}
  itemCount={rows.length}
  itemSize={70}
  width={totalColumnsWidth}
>
  {RenderRow}
</FixedSizeList>

useSortBy

  • Destructure useSortBy and pass it into our useTable hook to show we want to use it.

  • Pass through the column.getSortByToggleProps to the getHeaderGroupProps

<th {...column.getHeaderProps(column.getSortByToggleProps())}>
  • This works now, but lets add a icon to show where the sort is currently applied.
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
  {column.render("Header")}
  <span> {column.isSorted ? (column.isSortedDesc ? " 🔽" : " 🔼") : ""}</span>
</th>

useFilters

  • Deconstruct the useFilters hook and add - must be before the useSortBy hook

  • Create a default filter

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

  return (
    <input
      value={filterValue || ""}
      onChange={e => {
        setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely
      }}
      placeholder={`Search ${count} records...`}
    />
  );
}
  • Pass our column filter into our defaultColumn options
const defaultColumn = React.useMemo(
  () => ({
    width: 350,
    Filter: DefaultColumnFilter
  }),
  []
);
  • Render our column filter in the heading
<div>{column.canFilter ? column.render("Filter") : null}</div>