DEV Community

Paige Niedringhaus
Paige Niedringhaus

Posted on • Originally published at paigeniedringhaus.com on

Customize and Style Complex Data in React Table

Spreadsheet of data on a laptop screen

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Table showing Notecard location, timestamp, and nearest cell tower at the time.

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
    }
  }
Enter fullscreen mode Exit fullscreen mode

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.

EventTable

/* 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;
Enter fullscreen mode Exit fullscreen mode

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 and usePaginations 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 the useTable 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.

EventTable.module.scss

.tableWrapper {
  border-spacing: 0;
  border: 1px solid #ededed;
  width: 100%;

  tr:nth-child(2n) {
    background-color: #fafafa;
  }

  th {
    padding: 15px;
  }

  td {
    padding: 5px 10px;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

pages/index.tsx

// 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
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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.

pages/index.tsx

// 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)}
                  &#176;, 
                  {row.row.original.gps_location.longitude.toFixed(6)}&#176;
                </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
Enter fullscreen mode Exit fullscreen mode

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 the GPS 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

Latest comments (0)