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 thegetHeaderGroupProps
<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>