Introduction
This past summer, I started working for an Internet of Things (IoT) startup, Blues Wireless, that aims to make IoT development simpler - even when reliable Internet connections are not available. Blues does this via Notecards - prepaid cellular devices that can be embedded into any IoT device "on the edge" to transmit sensor data as JSON to a secure cloud: Notehub.
Frontend web development is what I specialize in, not hardware or firmware development, so as I get more familiar with IoT development, I started by building a simpler project: an asset tracker using a Blues Notecard, Blues Notecarrier AL with a built-in GPS antenna, and a small lithium-ion polymer (LiPo) battery to power the device.
With the help of the Blues developer experience docs, I had temperature, voltage, and GPS location data being delivered to the Notehub cloud from my tracker in less than half an hour. A good start, but the way that data from sensors really becomes useful is when it's displayed to users in some sort of UI. It could be maps, charts, or in my case, tables.
So I wanted to take my data from the Notehub cloud and pump it into a custom-made dashboard to track and display the Notecard's location and data readings from the real world. As a frontend developer, React is my current JavaScript framework of choice, and to get some more Next.js experience, I decided to build a Next.js Typescript-powered dashboard, and I learned a ton of interesting things in the process. Over the course of a series of blog posts (which I'll link to in this article), I'll share with you some of the cools things I discovered.
If you missed the first two installments of my blogs about building an asset tracking map in Next.js using React Leaflet, you can read it here.
The second one about making data visualization charts is available here.
This post will show you how to use the React Table library to make a data table and customize the data displayed in the cells, filled with real-world location data.
Here's what the final dashboard looks like - the table of location coordinates and data at the bottom is the focus for this particular post.
Create a table component in the Next.js app
Please note: This article will not go through the initial setup of a brand new Next.js app - that's outside the scope of this blog. If you're starting from scratch, I would recommend following the Next.js starter app with Typescript documentation.
If you'd prefer, you can also fork and download my whole, working code base from the GitHub repo here.
Install table dependencies
Let's kick off this post by adding the necessary table library to this Next.js app.
Although there's many React-focused table libraries and components to choose from, I went with the React Table library because it is a "headless" table-library that gives you a collection of lightweight, composable, and extensible hooks for building powerful tools and data grid experiences.
The "headless" part means that there's no markup or styles rendered via React Table, so you can customize and style it exactly as you want. It may sound a little odd at first, but it actually ends up working out really well as you'll see soon.
So, install React Table in the Next.js app by typing this command into the terminal.
$ npm install react-table
Note: React Table is compatible with React v16.8+, so please ensure your app's version of React is at least to here if not above.
Typescript Note:
If you're using Typescript in your project (like I am), you'll also want to install the following dev dependency to avoid Typescript errors:
$ npm install @types/react-table --save-dev
And now, we're ready to start using React Table to make a table in the application to display location data.
Create the <EventTable>
component and style it
The custom styled event table we'll be building.
For my table, I want to display "events" , which are what Blues Wireless's Notecards produce and send to its cloud Notehub. Each event is a separate JSON object, and although there are all sorts of different event types stored in a single project, the ones of concern today are the _track.qo
events.
_track.qo
event example
A typical _track.qo
event looks like this:
{
"uid": "d7cf7475-45ff-4d8c-b02a-64de9f15f538",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-05T16:10:52Z",
"received": "2021-11-05T16:11:29Z",
"body": {
"hdop": 3,
"seconds": 90,
"motion": 76,
"temperature": 20.1875,
"time": 1636123230,
"voltage": 4.2578125
},
"gps_location": {
"when": "2021-11-05T16:10:53Z",
"name": "Sandy Springs, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.913747500000014,
"longitude": -84.35008984375
}
}
It contains data like temperature
, time
, voltage
, and gps_location
coordinates. Useful stuff you might want to display in a table for easy readability, right?
Right. So here's how I built a reusable table component in a new file in the Next.js app named EventTable.tsx
.
I recommend copying the following code and pasting it into your own component file, and you can also click on the file title below to see the live code repo in GitHub.
/* eslint-disable react/jsx-key */
import { usePagination, useTable, Column } from "react-table";
import styles from "../../styles/EventTable.module.scss";
const EventTable = ({
columns,
data,
}: {
data: Array<any>;
columns: Array<Column>;
}) => {
const {
getTableProps, // table props from react-table
getTableBodyProps, // table body props from react-table
headerGroups, // headerGroups, if your table has groupings
prepareRow, // rows for the table based on the data passed
page, // Instead of using 'rows', we'll use page
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
},
usePagination
);
return (
<>
<h2>Tracker Events</h2>
<table className={styles.tableWrapper} {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column, _index) => (
<th {...column.getHeaderProps()}>{column.render("Header")}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})}
</tbody>
</table>
<div className="pagination">
<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 EventTable;
Let's go through all the things happening in this table component - the amount of props being destructured right off the bat can be a little overwhelming at first glance.
Although the actual import list from the react-table
library is very small - just three separate methods, the depth of functionality contained within expands quickly after the component's declared.
-
useTable
andusePaginations
combined make up all of the properties being destructured at the beginning of the component, which makes sense - this is a table libary we're working with, after all.
From these two hooks we get:
-
getTableProps
- the table props from react-table. -
getTableBodyProps
- the table body props from react-table. -
headerGroups
- headerGroups, if your table has groupings. -
prepareRow
- rows for the table based on the data passed. -
page
- necessary for having a paginated table. -
canPreviousPage
- boolean if there are previous pages the table can paginate to. -
canNextPage
- boolean if there are future pages the table can paginate to. -
pageOptions
- an array corresponding to available pages in the table (useful for select interfaces allowing users to type in a page number instead of using buttons). -
pageCount
- amount of pages available based on current page size value. -
gotoPage
- function to set page index to value specified by user. -
nextPage
- function to increase page index by one. -
previousPage
- function to decrease page index by one. -
setPageSize
- function to set page size to a new value. -
state: { pageIndex, pageSize }
- currently set page index and page size for table.
Whew! But after all that initial destructuring the <EventTable>
component only takes in two initial array props: columns
and data
. Note that both of these arrays of values must be memoized , according to the React Table docs.
-
data
is what theuseTable
hook turns into rows and pages of event data. -
columns
are the core columns configuration object for the entire table (put a pin in this array - we'll get to configuring it later in this article).
And after that it's all JSX in this component.
The headerGroups
are mapped over to render any headers at the very top of the table - our table will only have one header, but you could have multiple headers helping to visually show grouping of columns.
Then, each page
is mapped over, each row
of data in that page is extracted and each cell
in that row is rendered.
Followed by buttons galore and a custom input, which are added to make pagination possible in many different ways; canPreviousPage
, nextPage
, and goToPage
, for example.
And finally, pageSize
, or the number of rows displayed in each page, and is also made dynamic.
It's a lot of JSX, but it is fairly straightforward once some of the initial mapping and iterating is out of the way.
The nice part though, is that the react-table
library has all of this functionality built-in, and all we have to pass it in terms of data to get all this is two arrays. That's it.
Style the table
Now for some very simple styling to make this table presentable looking. As I mentioned before, React Table considers itself more of a headless table utility library, and as such, applies no default styling to any of the tables it generates out of the box.
Luckily, this also means there's no default styling to undo, so just a few lines of CSS can make this table look nice.
Wherever you're keeping styles in the Next.js app (I kept all of mine in the styles/
folder), create a new CSS or SCSS file named EventTable.module.css
.
I'm using Sass in my Next project in addition to the built-in CSS module styling.
There's very little Sass nesting going on in this particular SCSS file so I think it will be easy enough to follow, but if you'd like to set up Sass in your own Next project, you can follow Next's directions here.
.tableWrapper {
border-spacing: 0;
border: 1px solid #ededed;
width: 100%;
tr:nth-child(2n) {
background-color: #fafafa;
}
th {
padding: 15px;
}
td {
padding: 5px 10px;
}
}
In this SCSS file we're making a tableWrapper
class to give the table a light border (border: 1px solid #ededed;
), alternating the background color of the rows between the default background color or plain white with another off-white color (background-color: #fafafa;
) for better readability, and adding some padding to the table header (padding: 15px;
) and table data (padding: 5px 10px;
).
Not that much styling, but it makes for a polished-enough looking table that goes with the minimalist feel of the rest of the dashboard.
Note: A lot of this code for the
<EventTable>
was inspired by the very good examples in the React Table documentation, and I would strongly encourage you to check it out - it's very thorough and covers many of the table use cases people have.
Render the table in the app
With our table component done, it's time to add it to the main dashboard component. Import it into the index.tsx
file in your main pages/
folder.
I've condensed down the code in this file for clarity, but for the full code in GitHub, you can click on the file title here.
The actual data and columns for the table will be handled shortly - this is just to get the <EventTable>
into the dashboard page.
// imports
import EventTable from "../src/components/EventTable";
// other imports
type dataProps = {
// condensed for code brevity
};
export default function Home({ data }: { data: dataProps[] }) {
// logic to transform data into the columns and data arrays needed to pass to the table
return (
<div>
{/* extra tracker app code */}
<main>
<h1>React Blues Wireless Asset Tracker</h1>
{/* other tracker components */}
<div>
<EventTable columns={columns} data={eventTableData} />
</div>
</main>
</div>
);
}
// more code down here: getStaticProps
This is one of the easiest parts of this whole tutorial: import the <EventTable>
component at the top of the file, and then pop it into the JSX down below.
Now on to populating this table with events data.
Fetch data for the table
In my first asset tracking dashboard post I went into great detail about how to create your own asset tracker to generate real data for the app using Blues Wireless hardware and a data fetching function.
If you'd like to follow along there to build your own tracker and route data to Notehub, please be my guest.
For this post, I'll jump ahead to the part where we're already pulling data into the app via a Next.js getStaticProps
API call. The JSON data from the Notehub cloud looks like this:
[
{
"uid": "d7cf7475-45ff-4d8c-b02a-64de9f15f538",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-05T16:10:52Z",
"received": "2021-11-05T16:11:29Z",
"body": {
"hdop": 3,
"seconds": 90,
"motion": 76,
"temperature": 20.1875,
"time": 1636123230,
"voltage": 4.2578125
},
"gps_location": {
"when": "2021-11-05T16:10:53Z",
"name": "Sandy Springs, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.913747500000014,
"longitude": -84.35008984375
}
},
{
"uid": "3b1ef772-44da-455a-a846-446a85a70050",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-05T22:22:18Z",
"received": "2021-11-05T22:23:12Z",
"body": {
"hdop": 2,
"motion": 203,
"seconds": 174,
"temperature": 22,
"time": 1636150938,
"voltage": 4.2265625
},
"gps_location": {
"when": "2021-11-05T22:22:19Z",
"name": "Doraville, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.901052500000006,
"longitude": -84.27090234375
}
},
{
"uid": "e94b0c68-b1d0-49cb-8361-d622d2d0081e",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-05T22:40:04Z",
"received": "2021-11-05T22:46:30Z",
"body": {
"hdop": 1,
"motion": 50,
"seconds": 41,
"temperature": 21.875,
"time": 1636152004,
"voltage": 4.1875
},
"gps_location": {
"when": "2021-11-05T22:40:05Z",
"name": "Peachtree Corners, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.9828325,
"longitude": -84.21591015624999
}
},
{
"uid": "1344517c-adcb-4133-af6a-b1132ffc86ea",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-06T03:04:07Z",
"received": "2021-11-06T03:10:51Z",
"body": {
"hdop": 1,
"motion": 126,
"seconds": 218,
"temperature": 12.5625,
"time": 1636167847,
"voltage": 4.1875
},
"gps_location": {
"when": "2021-11-06T03:04:08Z",
"name": "Norcross, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.937182500000006,
"longitude": -84.25278515625
}
}
]
Each JSON object in this array is a separate _track.qo
motion event that displays the Notecard's current location and sensor readings. The part of the object that we care about in this particular post is the gps_location.latitude
, gps_location.longitude
, and body.voltage
values. This is the data our table needs.
If you'd like to save setting up your own asset tracker, you can paste this small set of events into your app's
index.js
file to simulate the data being fetched from Notehub, for testing purposes.
Transform the JSON data to fit custom table columns and cells
With data coming into our application, we'll manipulate it and create some new columns to display in the table. And this is where things start to get interesting.
Customize the cell data
If you notice from the shape of the JSON data coming in, the gps_location.latitude
and gps_location.longitude
values are two separate properties in the _track.qo
event, but it makes more sense to display them together in a single data cell as a comma separated list.
Likewise, the time
is in epoch time - a very machine readable format, that humans have a tough time deciphering. So for my table cells, it'd be nice to format it into a date that makes sense to people.
Here's how we'll do it - as always, the full code is available for you to review in GitHub.
// imports
import { useMemo, useEffect, useState } from "react";
import dayjs from "dayjs";
// other imports
type dataProps = {
// condensed for code brevity
};
export default function Home({ data }: { data: dataProps[] }) {
// state variables for the various pieces of data passed to the table
const [eventTableData, setEventTableData] = useState<dataProps[]>([]);
useEffect(() => {
if (data && data.length > 0) {
const eventData = [...data].reverse();
setEventTableData(eventData);
}
}, [data]);
interface row {
[row: { string }]: any;
}
const columns = useMemo(
() => [
{
Header: "Latest Events",
columns: [
{
Header: "Date",
accessor: "captured",
Cell: (props: { value: string }) => {
const tidyDate = dayjs(props.value).format("MMM D, YY h:mm A");
return <span>{tidyDate}</span>;
},
},
{
Header: "Voltage",
accessor: "body.voltage",
Cell: (props: { value: string }) => {
const tidyVoltage = Number(props.value).toFixed(2);
return <span>{tidyVoltage}V</span>;
},
},
{
Header: "Heartbeat",
accessor: "body.status",
},
{
Header: "GPS Location",
accessor: "gps_location",
Cell: (row) => {
return (
<span>
{row.row.original.gps_location.latitude.toFixed(6)}
°,
{row.row.original.gps_location.longitude.toFixed(6)}°
</span>
);
},
},
],
},
],
[]
);
return (
<div>
{/* extra tracker app code */}
<main>
<h1>React Blues Wireless Asset Tracker</h1>
{/* other tracker components */}
<div>
<EventTable columns={columns} data={eventTableData} />
</div>
</main>
</div>
);
}
// getStaticProps call to Notehub
To make this happen, we'll add in a few React Hooks: useState
, useEffect
and useMemo
, and to make formatting the time
easier, I added the library dayjs
to my project. Not entirely necessary, but it's convenient.
Get the table data
Inside of the component, create a new state variable with the useState
Hook to hold the event data: const [eventTableData, setEventTableData] = useState<dataProps[]>([]);
.
Next, set up a useEffect
function that will run when the JSON data is fetched into the component on page load via the getStaticProps
call. When the data
is present, we take the data, reverse the order so the most recent events are first instead of last - I think that makes more sense when seeing a list of events here: most recent first - and set those events in the component's state via setEventTableData
.
Configure the columns
The final step is defining the columns for the table.
The first thing we must do is wrap the newly defined columns
variable in a useMemo
function (as defined by the React Table documentation), and I'll also give the whole table a header named Latest Events
.
Then we can set up the columns
array: I want one for the Date
stamp, one to display the device's Voltage
, one for the device's Status
(i.e. whether the event is a motion event caused by actual motion or it's a "heartbeat" event - an event just to let you know the device is still alive), and one for the GPS Location
, which as I said before I want to be a combination of latitude and longitude.
Each of these titles will become a Header
property in each of these objects, and they will be followed by an accessor
property. Typically, the accessor
value will be something simple like body.status
, which is the way to get the actual value out of the object. If the accessor
, however, needs to be transformed before being set, this is where the Cell
function comes into play.
Cell is a method that can be be used to transform the props in that cell and completely customize the return value, right down to the JSX.
So for example, to modify the timestamp date into a nicely formatted string, we grab the captured
value out of the event, and then use Cell
to run a dayjs
function and format the date into a new variable named tidyDate
. And then we return tidyDate
as a bit of JSX wrapped in a <span>
tag.
Similarly, to concatenate the latitude and longitude values together into one cell, we have to go even deeper into the props being passed to Cell
, and pull out two of the properties nested inside of the gps_location
object to return in the cell.
Console.log()
is your friend here.Don't be afraid to reach for
console.log()
when putting together these complex data cells. Only by using it to see the shape of the data and each row in the table was I able to figure out how to extract the values I needed for theGPS Location
column.
And there you have it: our columns
are defined and our data
is too, pass both to the <EventTable>
component, and we should be good to go.
Conclusion
About 9 months ago, I began working for an IoT startup, and learning how to manage and display readings in dashboards from devices "on the edge". So one of the first projects I built to this end was a dashboard including data charts, a map showing device location, and a table listing all the events being displayed here.
To render a table with all the flexibility and none of the cruft, I turned to the React Table library, a headless and highly extensible set of hooks that simplifies building complex tables, handling things like pagination, page size, and so much more, with very little effort on our part. Even modifying the display of table cell data in a row, is relatively straightforward. It's a great library.
This dashboard actually came in handy when my parents' car was stolen from their driveway the night after Thanksgiving. If you want to hear the whole story and build your own tracker, check out this blog post and video I made for Blues Wireless - it details the whole process from hardware to software to deploying to Netlify.
Check back in a few weeks β Iβll be writing more about JavaScript, React, IoT, or something else related to web development.
If youβd like to make sure you never miss an article I write, sign up for my newsletter here: https://paigeniedringhaus.substack.com
Thanks for reading. I hope you enjoyed learning how to leverage the React Table library to make a data table to display event data from an IoT asset tracker. Tables can be tricky to get right, but they're such a nice, straightforward way to display all sorts of data to users.
References & Further Resources
- Hackster.io original asset tracker project
- Asset tracker GitHub repo
- React Table library
- Blues Wireless website
- Blues Wireless developer experience site
- Original article about rendering maps in a Next.js application
- Follow up article about custom styling charts and tooltips with React Recharts library
Top comments (0)