Written by John Au-Yeung✏️
It’s widely acknowledged that creating a table with React is a pain. No surprise, then, that there are many libraries to make creating tables easier for React apps.
One of these packages that aims to make our lives easier is react-table. It provides a modern, Hooks-based API to let us create tables with React with little hassle.
In this article, we’ll look at how to use react-table to add tables to our React app.
Creating a basic table
Creating a basic table in a React app is easy with react-table. Run the following to install it:
npm i react-table
Then we can use it as follows:
import React from "react";
import { useTable } from "react-table";
const data = [
{ firstName: "jane", lastName: "doe", age: 20 },
{ firstName: "john", lastName: "smith", age: 21 }
];
const columns = [
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
accessor: "lastName"
}
]
},
{
Header: "Other Info",
columns: [
{
Header: "Age",
accessor: "age"
}
]
}
];
const Table = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = useTable({
columns,
data
});
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, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
})}
</tbody>
</table>
);
};
export default function App() {
return (
<div className="App">
<Table columns={columns} data={data} />
</div>
);
}
In the code above, we imported the useTable
Hook from the react-table package. Then we created the data
to populate the table with data:
const data = [
{ firstName: "jane", lastName: "doe", age: 20 },
{ firstName: "john", lastName: "smith", age: 21 }
];
We just put properties in objects to add additional data for a table row.
We can create columns in a list with the following code:
const columns = [
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
accessor: "lastName"
}
]
},
{
Header: "Other Info",
columns: [
{
Header: "Age",
accessor: "age"
}
]
}
];
The Header
property has the string for the names that’ll be displayed, and the accessor
property is the property name that’s in the array entry objects.
In the Table
component code, we have:
const Table = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = useTable({
columns,
data
});
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, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
})}
</tbody>
</table>
);
};
The useTable
Hook takes the column
and data
from the props, which originate from those objects and arrays that we defined earlier. We get the functions from the getTableProps
and getTableBodyProps
from the object returned from the useTable
Hook.
The getHeaderProps()
function is called inside the th
tags and spread to populate the headers. With this, we pass the props returned by the getTableBodyProps()
function to tbody
to spread the props to properly style and align the columns.
The prepareRow(row);
returned from the useTable
Hook creates the row entries, which can be automatically populated after the call to the function changes the row
object in place.
Then we have:
{rows.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
})}
This automatically populates the cells by getting the items from the getCellProps()
method and then populating the values from the returned object. We called cell.render("Cell")
to render each td
as a cell.
Finally, in App
, we used the Table
component, which takes the column
and data
props. The values are the columns
and data
objects that we created earlier.
The items displayed in the table in two panes. The left pane has the Name header with two columns: First Name and Last Name. Then, the right pane has the Other Info heading with the Age column.
Adding a footer
We can add a footer by adding a Footer
property to out column objects. We can write the following code to do that:
import React from "react";
import { useTable } from "react-table";
const data = [
{ firstName: "jane", lastName: "doe", age: 20 },
{ firstName: "john", lastName: "smith", age: 21 }
];
const columns = [
{
Header: "Name",
Footer: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
accessor: "lastName"
}
]
},
{
Header: "Other Info",
Footer: "Other Info",
columns: [
{
Header: "Age",
accessor: "age",
Footer: info => {
const total = React.useMemo(
() => info.rows.reduce((sum, row) => row.values.age + sum, 0),
[info.rows]
);
return <>Average Age: {total / info.rows.length}</>;
}
}
]
}
];
const Table = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
footerGroups,
rows,
prepareRow
} = useTable({
columns,
data
});
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, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
})}
</tbody>
<tfoot>
{footerGroups.map(group => (
<tr {...group.getFooterGroupProps()}>
{group.headers.map(column => (
<td {...column.getFooterProps()}>{column.render("Footer")}</td>
))}
</tr>
))}
</tfoot>
</table>
);
};
export default function App() {
return (
<div className="App">
<Table columns={columns} data={data} />
</div>
);
}
In the code above, we added the Footer
property to the columns
array as follows:
const columns = [
{
Header: "Name",
Footer: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
accessor: "lastName"
}
]
},
{
Header: "Other Info",
Footer: "Other Info",
columns: [
{
Header: "Age",
accessor: "age",
Footer: info => {
const total = React.useMemo(
() => info.rows.reduce((sum, row) => row.values.age + sum, 0),
[info.rows]
);
return <>Average Age: {total / info.rows.length}</>;
}
}
]
}
];
We added the Footer
property to the top-level of each object.
Also, we add a function for the Footer
property in the object for the Age column.
The Footer
property in the object for the Age column is:
info => {
const total = React.useMemo(
() => info.rows.reduce((sum, row) => row.values.age + sum, 0),
[info.rows]
);
return <>Average Age: {total / info.rows.length}</>;
}
It takes the info
object, which has all the table data. Then we summed by all the age
property values for each entry and divided it by info.row.length
to return the average age. This is displayed at the bottom of the table below the Age column.
The average will change as the row changes since we have [info.rows]
, which watches the rows for changing values and recomputes the value when the rows change.
Sorting
We can add sorting to a table by calling a few functions. We have to pass in the useSortBy
Hook as the second argument of the useTable
Hook to get sorting capability in our table.
Then, in our JSX code, we have to pass in column.getSortByToggleProps()
to column.getHeaderProps
to get the sorting order of the columns in the rendered column.
We can check the order in which a column is sorted by using the column.isSorted
and column.isSortedDesc
to check if a column is sorted by ascending or descending order, respectively.
Also, we can add a sortType
property to the column array entries so we can specify the sort type. For instance, we can write the following code to add basic sorting to our table:
import React from "react";
import { useTable, useSortBy } from "react-table";
const data = [
{ firstName: "jane", lastName: "doe" },
{ firstName: "john", lastName: "smith" }
];
const columns = [
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName",
sortType: "basic"
},
{
Header: "Last Name",
accessor: "lastName",
sortType: "basic"
}
]
}
];
const Table = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = useTable(
{
columns,
data
},
useSortBy
);
return (
<table {...getTableProps()}>
<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>
<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>
);
};
export default function App() {
return (
<div className="App">
<Table columns={columns} data={data} />
</div>
);
}
In the code above, we specified that sortType
is 'basic'
so that words are sorted alphabetically and numbers are sorted numerically.
Then we rendered the thead
by writing:
<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>
This adds icons for indicating the sort order of each column and getting the order by which the column is sorted.
After writing that code, we’ll see a sort button to the right of our column headings that we can click on to sort the columns.
Filtering
Filtering is more complex than creating a simple table or sorting. We have to create a component with an input control we can use to filter our items. The input component will take the functions that are returned from the useTable
as props and call them in the inputs.
For instance, we can write the following code to do that:
import React from "react";
import { useTable, useFilters, useGlobalFilter } from "react-table";
const data = [
{ firstName: "jane", lastName: "doe", age: 20 },
{ firstName: "john", lastName: "smith", age: 21 }
];
const columns = [
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName",
filter: "text"
},
{
Header: "Last Name",
accessor: "lastName",
filter: "text"
}
]
},
{
Header: "Other Info",
columns: [
{
Header: "Age",
accessor: "age",
filter: "text"
}
]
}
];
const DefaultColumnFilter = ({
column: { filterValue, preFilteredRows, setFilter }
}) => {
const count = preFilteredRows.length;
return (
<input
value={filterValue || ""}
onChange={e => {
setFilter(e.target.value || undefined);
}}
placeholder={`Search ${count} records...`}
/>
);
};
const GlobalFilter = ({
preGlobalFilteredRows,
globalFilter,
setGlobalFilter
}) => {
const count = preGlobalFilteredRows && preGlobalFilteredRows.length;
return (
<span>
Search:{" "}
<input
value={globalFilter || ""}
onChange={e => {
setGlobalFilter(e.target.value || undefined); // Set undefined to remove the filter entirely
}}
placeholder={`${count} records...`}
style={{
border: "0"
}}
/>
</span>
);
};
const Table = ({ columns, data }) => {
const filterTypes = React.useMemo(
() => ({
text: (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id];
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.startsWith(String(filterValue).toLowerCase())
: true;
});
}
}),
[]
);
const defaultColumn = React.useMemo(
() => ({
Filter: DefaultColumnFilter
}),
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
state,
visibleColumns,
preGlobalFilteredRows,
setGlobalFilter
} = useTable(
{
columns,
data,
defaultColumn,
filterTypes
},
useFilters,
useGlobalFilter
);
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>
{column.render("Header")}
<div>{column.canFilter ? column.render("Filter") : null}</div>
</th>
))}
</tr>
))}
<tr>
<th
colSpan={visibleColumns.length}
style={{
textAlign: "left"
}}
>
<GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={state.globalFilter}
setGlobalFilter={setGlobalFilter}
/>
</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>
);
};
export default function App() {
return (
<div className="App">
<Table columns={columns} data={data} />
</div>
);
}
In the code above, we added the GlobalFilter
component as follows:
const GlobalFilter = ({
preGlobalFilteredRows,
globalFilter,
setGlobalFilter
}) => {
const count = preGlobalFilteredRows && preGlobalFilteredRows.length;
return (
<span>
Search:{" "}
<input
value={globalFilter || ""}
onChange={e => {
setGlobalFilter(e.target.value || undefined);
}}
placeholder={`${count} records...`}
style={{
border: "0"
}}
/>
</span>
);
};
This is used to search all the columns present in the data by calling the setGlobalFilter
function that’s passed in as props. The preGlobalFilteredRows
is an array in which we can count the number of rows that we’re searching for.
Then, in the Table
component, we added the following code:
const filterTypes = React.useMemo(
() => ({
text: (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id];
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.startsWith(String(filterValue).toLowerCase())
: true;
});
}
}),
[]
);
const defaultColumn = React.useMemo(
() => ({
Filter: DefaultColumnFilter
}),
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
state,
visibleColumns,
preGlobalFilteredRows,
setGlobalFilter
} = useTable(
{
columns,
data,
defaultColumn,
filterTypes
},
useFilters,
useGlobalFilter
);
The defaultColumn
has a cached object, which has the DefaultColumnFilter
set as follows:
const DefaultColumnFilter = ({
column: { filterValue, preFilteredRows, setFilter }
}) => {
const count = preFilteredRows.length;
return (
<input
value={filterValue || ""}
onChange={e => {
setFilter(e.target.value || undefined);
}}
placeholder={`Search ${count} records...`}
/>
);
};
The defaultColumn
caches the input component that’s used to search individual columns. We also have the filterTypes
constant, which has the cached value of the filter we used to search our table.
We have an object with the text
method, which is used to search the entries that we’re looking for as we type. In the method, we called filter
on rows
to return the items that start with the given search string, which is stored in filterValue
.
We also used more of the returned properties from the useTable
Hook and passed in more arguments to the Hook, including the useFilters
and useGlobalFilter
Hooks to let us filter by column and globally, respectively.
Also, we added the defaultColumn
and filterTypes
objects to the object in the first argument to let us set the component we’ll use to do the filtering by default. filterTypes
lets us set the value to the name of our function that we created for returning filtered data from our array of data.
In the end, we get two inputs to filter each column individually and one that can filter the items from all columns globally.
Pagination
We can add pagination using the usePagination
Hook, which is passed in as the argument for the useTable
Hook.
The useTable
Hook then returns a bunch of pagination-related variables that we used to track the pagination and navigate to different pages.
To make a simple table with pagination, we can write the following code:
import React from "react";
import { useTable, usePagination } from "react-table";
const firstNames = ["jane", "john", "alex"];
const lastName = ["smith", "jones"];
const data = Array(100)
.fill()
.map(a => ({
firstName: firstNames[Math.floor(Math.random() * firstNames.length)],
lastName: lastName[Math.floor(Math.random() * lastName.length)],
age: Math.ceil(75 * Math.random())
}));
const columns = [
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
accessor: "lastName"
}
]
},
{
Header: "Other Info",
columns: [
{
Header: "Age",
accessor: "age"
}
]
}
];
const Table = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize }
} = useTable(
{
columns,
data,
initialState: { pageIndex: 0 }
},
usePagination
);
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()}>
{page.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})}
</tbody>
</table>
<div>
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{"<<"}
</button>{" "}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{"<"}
</button>{" "}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{">"}
</button>{" "}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{">>"}
</button>{" "}
<span>
Page{" "}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{" "}
</span>
<span>
| Go to page:{" "}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
gotoPage(page);
}}
style={{ width: "100px" }}
/>
</span>{" "}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</>
);
};
export default function App() {
return (
<div className="App">
<Table columns={columns} data={data} />
</div>
);
}
In the code above, we generated 100 array entries with random names and pages of people. The headers are the same as the simple table example above.
In the Table
component, we have:
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize }
} = useTable(
{
columns,
data,
initialState: { pageIndex: 0 }
},
usePagination
);
With this, we can get various pieces of data we need for pagination, like pageSize
to change the number of items displayed on each page.
canPreviousPage
and canNextPage
tell us whether we can move to the previous or next page respectively. pageCount
has the total page count, and gotoPage
is a function that lets us skip to the given page number. previousPage
and nextPage
are also functions that let us navigate to the given page.
They’re used in the following div
to navigate between pages:
<div>
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{"<<"}
</button>{" "}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{"<"}
</button>{" "}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{">"}
</button>{" "}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{">>"}
</button>{" "}
<span>
Page{" "}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{" "}
</span>
<span>
| Go to page:{" "}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
gotoPage(page);
}}
style={{ width: "100px" }}
/>
</span>{" "}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
Then we get a table with the same columns as in the example above, but with pagination buttons added. We can also use the dropdown to change the size of each page.
Integration with Material UI
The react-table
package integrates with Material UI to let us create a table that follows the Material Design specification.
To install Material UI, we run:
npm install @material-ui/core
Then we can use Material UI’s table components with react-table
to create the table as follows:
import React from "react";
import { useTable } from "react-table";
import MaUTable from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
const data = [
{ firstName: "jane", lastName: "doe", age: 20 },
{ firstName: "john", lastName: "smith", age: 21 }
];
const columns = [
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
accessor: "lastName"
}
]
},
{
Header: "Other Info",
columns: [
{
Header: "Age",
accessor: "age"
}
]
}
];
const Table = ({ columns, data }) => {
const { getTableProps, headerGroups, rows, prepareRow } = useTable({
columns,
data
});
return (
<MaUTable {...getTableProps()}>
<TableHead>
{headerGroups.map(headerGroup => (
<TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<TableCell {...column.getHeaderProps()}>
{column.render("Header")}
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{rows.map((row, i) => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<TableCell {...cell.getCellProps()}>
{cell.render("Cell")}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
</MaUTable>
);
};
export default function App() {
return (
<div className="App">
<Table columns={columns} data={data} />
</div>
);
}
In the code above, we used the Material UI components to render the table, but the data is populated by react-table
. We called the same methods we used in the simple table example to populate the rows and columns with data.
Therefore, we get the same data and columns as the simple table example, but it adheres to Material Design instead of having no styling.
Conclusion
As we can see, react-table is capable of creating tables with lots of capabilities without having to create everything from scratch ourselves.
It provides us with a Hooks-based API to create tables, which is important since some devs would like to switch to using function components with Hooks now.
There are many more examples to showcase what react-table can do on its official GitHub repo. Some examples are simplified from the examples on their official website.
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
The post Building and styling tables with react-table v7 appeared first on LogRocket Blog.
Top comments (0)