Data Visualization With Tables in React
Working with tables in React has always been a burden since developers encounter issues related to the styling, responsiveness and proper rendering of data on tables.
<!--more-->
This article will look at an approach that would enable us to properly visualize data in tables with an npm package known as react-table
.
React table is a library that focuses on the React Hooks pattern, therefore abstracting the need to create or hard-code HTML table elements.
Prerequisites
To follow along with this article, you should know the following:
- React; a JavaScript framework used for creating single-page applications
- The JavaScript spread operator,
- PropTypes validation in React,
- Props handling in React,
- React Hooks,
- React styled-components.
Getting started
The scope of this article is around the react framework known as Nextjs
. We can also use create-react-app
to build this project.
npx create-next-app [your-app-name]
The file structure of a next.js app is quite different from create-react-app
's file architecture.
We will only be interacting with the files we need in this project to avoid being overwhelmed.
|--pages
| |-- _app.js
| |-- index.js
|--src
| |-- components
| | |-- Table.js
| |-- data.json
| |-- columns.js
|__
Now that we are familiar with the file structure above. Let us continue by installing the styled-components
and react-table
packages.
npm install styled-components react-table
An overview of the components and files
_app.js
: is the root file of the application. It looks similar to the index.js
file in create-react-app
.
In this file, you can:
- apply any global style(s)
- add new themes
- provide context to the whole application
- import the
redux
provider context to manage the state of your application (if you are using redux)
import Head from "next/head";
import React from "react";
function MyApp({ Component, pageProps }) {
return (
<React.Fragment>
<Head>
<meta name="theme-color" content="#3c1756" />
<title>A React Table</title>
</Head>
<Component {...pageProps} />
</React.Fragment>
);
}
export default MyApp;
In the first _app.js
snippet, the Head
component gets imported from "next/head"
. This component performs the same function that the normal HTML <head>
element does.
In the Head
component, we can add different child elements such as the <title>
element which describes the current route or page, the <link>
element which imports stylesheets or set the page’s favicon, the <meta>
tags works for SEO optimization.
index.js
: is our default route.
When running the development server, anything we do in the file gets displayed at this address: https://localhost:3000/
.
npm run dev
Table.js
: is the component that holds the UI of the data that maps fromdata.json
andcolumns.js.
data.json
: holds the array of user objects that will be rendered in theTable
component. We would use the data copied from the Jsonplaceholder user API instead of writing the API calls ourselves.
[
{
"id": 1,
"name": "Leanne Graham",
"email": "Sincere@april.biz",
"username": "Bret",
"phone": "1-770-736-8031 x56442",
"company_name": "Romaguera-Crona",
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv",
"phone": "010-692-6593 x09125",
"company_name": "Romaguera-Crona",
},
// the remaining user objects fall below
];
We are reducing the number of user objects in the array so that this article can be shorter.
However, if you want to use all the objects, you can get them here.
The API endpoint provides a list of ten user objects in the array per API call.
columns.js
: is also an array of objects that stores the items we want to render on the table's header.
export const COLUMNS = [
{
Header: "S/N",
accessor: "id",
},
{
Header: "Fullname",
accessor: "name",
},
{
Header: "Email address",
accessor: "email",
},
{
Header: "Username",
accessor: "username",
},
{
Header: "Phone number",
accessor: "phone",
},
{
Header: "Company",
accessor: "company_name",
},
];
Notice how we declared the accessor
key in the objects. The accessor
is more like the locator of the property that is in src/data.json
.
Let us compare both objects below:
// columns.js
{
Header: "Company",
accessor: "company_name",
},
// data.json
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv"
"phone": "010-692-6593 x09125",
"company_name": "Romaguera-Crona"
},
The accessor
property in column.js
has a value of company_name
, which in turn renders the corresponding property’s value from the json file.
Putting the components together
In the last section, we installed the necessary dependencies that we need in our next.js app.
Additionally, we have also seen the contents of the files that hold the table’s data and how they function.
This section will start looking at the content of Table.js
in the component
folder below:
import React from "react";
import styled from "styled-components";
// the styled component that serves as a
// wrapper for the table component goes here
const TableContainer = styled.div`
table {
width: 100%;
height: 100%;
border-collapse: collapse;
box-shadow: 1px 3px 5px rgba(0, 0, 0, 0.08);
}
thead {
height: 64px;
background: #3c1742;
}
thead th {
font-size: 14px;
color: ;
text-align: left;
padding: 0 30px;
}
tr {
height: 64px;
border-bottom: 2px solid grey;
}
tr td {
padding: 0 30px;
border-bottom: 1px solid #3c1742;
}
@media only screen and (max-width: 992px) {
table {
white-space: nowrap;
}
}
`
const Table = ({ columns, data }) => {
// react-table hooks will go here
return (
// Table's JSX will go here
);
}
export default Table;
The table component above focuses on the style of the table.
Let us take a look at some of the properties below.
The table
selector sets the width and height of the table to 100%
, making it responsive to receive more columns and rows:
table {
width: 100%;
margin-top: 20px;
height: 100%;
}
The media query rule sets the table's text-wrap
property to no-wrap
at a maximum screen width of 992px
. Find more about media queries here.
@media only screen and (max-width: 992px) {
table {
white-space: nowrap;
}
}
Working on the table component
import React from "react";
import { useTable } from "react-table";
import styled from "styled-component";
const TableContainer = styled.div`
...
`;
const Table = ({ columns, data }) => {
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable({ columns, data });
return (
<TableContainer>
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup, index) => (
<tr {...headerGroup.getHeaderGroupProps()} key={index}>
{headerGroup.headers.map((column, index) => (
<th key={index} {...column.getHeaderProps()}>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row, index) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()} key={index}>
{cell.render("Cell")}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</TableContainer>
);
};
export default Table;
Let us break the component above into smaller chunks.
- The snippet below illustrates the process of creating the table instance using the
useTable()
hook from thereact-table
library.
import { useTable } from "react-table";
const Table = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = useTable({ columns, data, });
return (
...
);
}
export default Table;
We passed columns
and data
as props to the useTable()
hook because it must function correctly.
- The destructuring assignment of the
useable()
function gives us access to the table’s instance methods.
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable({ columns, data });
The props are also required to be memoized. This process can be done with the use of the useMemo()
hook. We will get to the reasons why in the next section.
- The
getTableProps()
is among the methods of the table instance we created with theuseTable()
Hook. Notice how we passed it as an attribute to the<table>
element. That syntax(…)
is called thespread operator
.
const Table = ({ columns, data }) => {
const { ... } = useTable({ columns, data, });
return (
<TableContainer>
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup, index) => (
<tr {...headerGroup.getHeaderGroupProps()} key={index}>
{headerGroup.headers.map((column, index) => (
<th key={index} {...column.getHeaderProps()}>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead>
...
</table>
</TableContainer>
);
};
export default Table;
When we apply …getTableProps()
as an attribute to the table
element, we are making the other properties of this method (the getTableProps()
method) to be accessible for us.
That is why we can do something like this:
<thead>
{headerGroups.map((headerGroup, index) => (
<tr {...headerGroup.getHeaderGroupProps()} key={index}>
{headerGroup.headers.map((column, index) => (
<th key={index} {...column.getHeaderProps()}>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead>
Let us take a look at this example below which illustrates the process of a spread operation.
// a daughter array with an object in it
// showing the properties of this person
let daughter = [
{
firstname: "Bola",
lastname: "Jones",
hair_color: "black",
role: "daughter",
},
];
// a family array showing the list of members
// in a family and their properties.
let family = [
{
firstname: "Dapo",
lastname: "Jones",
hair_color: "black",
role: "Father",
},
{
firstname: "Teni",
lastname: "Jones",
hair_color: "red",
role: "Mother",
},
...daughter,
];
console.log(family);
With the spread operator, the daughter
array is added to the family
array. It can be accessed by looping through the array with the map()
function or using the array syntax.
console.log(family[2].role); // prints daughter to the console
To learn more about the spread operator, click here.
To also learn more about the methods associated with the table instance, click here.
The index page component
In the previous sections, we looked at the contents of each component, the function that they all perform, the useTable()
hook and the spread operator's functions, and how the table instance methods work behind the scenes.
In this section, we will be covering the final steps of this process.
First, we will be importing the Table
component into pages/index.js
and adding a custom scrollbar visible on desktop modes.
Let us get started.
We are importing the COLUMNS
module alongside the tableData
from their respective files:
import React from "react";
import styled from "styled-components";
import Table from "../../components/Table";
import { COLUMNS } from "../../src/Columns";
import tableData from "../../src/table_data.json";
Now, let us look at the styled component that is wrapping the index page; we will also go over the function of some essential style blocks.
import styled from "styled-components";
const TableWrapper = styled.section`
padding: 0 150px 0 150px;
padding-bottom: 130px;
h1 {
text-align: center;
color: #000;
font-size: 36px;
font-weight: 900;
padding: 50px 0 0 0;
}
.table-container {
height: 700px;
overflow: auto;
}
.table-container::-webkit-scrollbar {
width: 10px;
}
.table-container::-webkit-scrollbar-thumb {
height: 4px;
background: #3c1742;
border-radius: 10px;
}
.table-container::-webkit-scrollbar-track {
border: 1px solid #3c1742;
border-radius: 10px;
}
@media only screen and (max-width: 992px) {
padding: 0 15px 0 15px;
padding-bottom: 130px;
.table-container::-webkit-scrollbar {
display: none;
}
}
The snippet below represents the custom scrollbar style:
.table-container::-webkit-scrollbar {
width: 10px;
}
.table-container::-webkit-scrollbar-thumb {
height: 4px;
background: #3c1742;
border-radius: 10px;
}
.table-container::-webkit-scrollbar-track {
border: 1px solid #3c1742;
border-radius: 10px;
}
The snippet below makes the table responsive on mobile and desktop screens. However, on mobile screens, the custom scrollbar is hidden.
.table-container {
height: 700px;
overflow: auto;
}
@media only screen and (max-width: 992px) {
.table-container::-webkit-scrollbar {
display: none;
}
}
Now that we understand the styles, let us look at the primary component that renders the complete data on the UI.
import React from "react";
import { COLUMNS } from "../../src/Columns";
import tableData from "../../src/table_data.json";
import styled from "styled-components";
const TableWrapper = styled.section``;
const UserTable = () => {
return (
<TableWrapper>
<h1>Leaderboard</h1>
<div className="table-container">
<Table
columns={React.useMemo(() => COLUMNS)}
data={React.useMemo(() => tableData)}
/>
</div>
</TableWrapper>
);
};
export default UserTable;
You notice how we passed the columns
and data
props to the Table
component, and how we assigned their values to the useMemo()
hook.
The useMemo()
ensures that the data is not re-created at every render.
If we do not set it that way, react-table
will think it is receiving new data, which would result in recompiling the code, which intern leads to poor performance.
Here is the outcome of the project:
I hope this article guided you well on how to visualize data on a table in React.
Conclusion
This article provided a head-start on working with data tables in React.
We developed an application using react and implemented the react-table
library to demonstrate the concept.
Working on this project should be an insight into building responsive data tables in react.
Peer Review Contributions by: Jerim Kaura