Skip to main content Skip to footer

What's the Difference Between React Data Grids and Data Tables and Why Do You Need Them

Most front-end developers are familiar with this scenario: a new project for a data-driven application starts. Everyone is convinced that the design must be as simple and straightforward as possible. At its core, a simple table - a couple of columns and rows of data. But even before the minimum viable application ships, it becomes clear that a simple table is not enough. The stakeholders come up with more requirements, such as pagination and filtering. The designers demand user personalization and customizability. Its at this moment when developers look for help in the form of an existing data grid.

In this article, we'll learn how to integrate a data grid in a React application, as well as look at some best practices for integrating data into the grid, from importing files to connecting with APIs and databases. We'll be covering the following:

Ready to Create Custom React Applications? Download Wijmo Today!

Data Grid Features

Data Grids vs. Data Tables

In its most basic form, a data grid could be seen as a table - data represented in rows and columns. Differences start already at basic functionality, like scrolling. While a table would not offer much more than a sticky header, usually showing the column definitions, the data grid can be much more sophisticated. The same pattern continues to sort (multi-column with precedence) and data selection. The latter is now cell-based instead of row-based.

Another important feature that many data grids offer is a data export function. In the simplest case, this is equivalent to copying the data to a clipboard. However, some requirements may include things such as exporting the data to a CSV document or an Excel XLSX file.

In general, data grids support interoperability with standard spreadsheet applications such as Excel, which can boost productivity. Bundled together with real-time updates and backend-supported collaboration techniques, this makes data grids real data manipulation beasts. It is no coincidence that Microsoft uses the Excel 365 engine in almost all other online data editing tools, such as their Power BI tools.

Features that truly distinguish data grids from tables are, for instance, custom cell renderings and formatting capabilities. Here we could think of charts or other rich visualizations shown in specific cells. Another example would be quick visual hints, such as sparklines.

Last, but certainly not least, there is a strong demand for accessibility features. Data grids offer support for cell highlighting, touch support, overlay icons, and keyboard navigation that comes close to or exceeds the capabilities of native spreadsheet applications.

Whats the Difference Between React Datagrids and Data Tables and Why Do You Need Them

Rolling Your Own Data Grid in React

The React ecosystem includes dozens of viable data grid components. These enable you to access all the prepackaged functionality with just a few lines of code. Before we dive into using the available solutions. let's see how we could implement a proper data grid from scratch.

Since every data grid has a table at its core, we'll start with that. There are two essential ways to design a table in React:

  1. Follow the typical HTML abstraction layer and creating components such as TableContainer using children: TableHeader, TableFooter, TableRow, and TableCell.
  2. Have a single Table component using render props and other specialized props, for adjusting the target rendering.

While the first option is an exceptional approach for having a simplistic yet consistently styled table, the second option - a Table component with render props - is capable of so much more by transitioning much of the representation logic into an abstraction layer. Therefore, it is the path usually chosen with existing solutions.

Let's see a simple implementation of the first approach, without error handling and other exciting features:

import * as React from "react";

const TableContainer = ({ striped, children }) => (
  <table className={striped ? "table-striped" : ""}>{children}</table>
);

const TableHeader = ({ children }) => <thead>{children}</thead>;

const TableBody = ({ children }) => <tbody>{children}</tbody>;

const TableRow = ({ children }) => <tr>{children}</tr>;

const TableCell = ({ children }) => <td>{children}</td>;

const MyTable = () => (
  <TableContainer striped>
    <TableHeader>
      <TableRow>
        <TableCell>ID</TableCell>
        <TableCell>Name</TableCell>
        <TableCell>Age</TableCell>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow>
        <TableCell>1</TableCell>
        <TableCell>Foo</TableCell>
        <TableCell>21</TableCell>
      </TableRow>
      <TableRow>
        <TableCell>2</TableCell>
        <TableCell>Bar</TableCell>
        <TableCell>29</TableCell>
      </TableRow>
    </TableBody>
  </TableContainer>
);

The idea is that the individual components, such as TableContainer, could expose all of the different options via their props. As such, the MyTable Component could use these props directly instead of via cryptic class names or weird attributes.

Now, following the second approach, the previous example looks a bit different:

import * as React from "react";

const Table = ({ striped, columns, data, keyProp }) => (
  <table className={striped ? "table-striped" : ""}>
    <thead>
      <tr>
        {columns.map((column) => (
          <th key={column.prop}>{column.label}</th>
        ))}
      </tr>
    </thead>
    <tbody>
      {data.map((row) => (
        <tr key={row[keyProp]}>
          {columns.map((column) => (
            <td key={column.prop}>{row[column.prop]}</td>
          ))}
        </tr>
      ))}
    </tbody>
  </table>
);

const MyTable = () => (
  <Table
    striped
    keyProp="id"
    columns={[
      { label: "ID", prop: "id" },
      { label: "Name", prop: "name" },
      { label: "Age", prop: "age" },
    ]}
    data={[
      { id: 1, name: "Foo", city: "", age: 21 },
      { id: 2, name: "Bar", city: "", age: 29 },
    ]}
  />
);

As you can see, the logic in the Table component is much more abstracted. The rendering cost is also higher. However, this could be controlled and optimized quite nicely, for example, by caching parts using techniques such as useMemo.

The most significant advantage of this approach is undoubtedly the data-driven aspect. Instead of constructing the table entirely on your own, you can just insert some data and get a rendered table back.

You can go from this version to a full data grid component, leveraging the same principles. However, today, with the availability of so many UI libraries, there is very little reason to spend time developing your own data grid in-house.

Data Grid Controls Handle the Hard Work

Rather than reinventing the wheel to build a table programmatically - and still be stuck with the limitations of an HTML table - the best choice is to incorporate a data grid control. There are several excellent open-source choices, including:

  • React Virtualized
  • React Data Grid
  • React Table

There are many others, each usually appealing to its creators' specific needs - as is often the case with open source projects.

While open-source options are appealing, commercial offerings like Wijmo offer distinct advantages for React data grid components. The FlexGrid included with the Wijmo UI React library is the best plug-and-play data grid for React.

One advantage is the broad feature set included by default with the data grid. Another is the promise of support and ongoing development.

Ready to Create Custom React Applications? Download Wijmo Today!

A Basic React Data Grid Control in Action

Let's start by looking at a simple data grid visualization representing some data, including a visual hint. I'm going to use some arbitrary dates-and-counts data representing the kind of dataset we're all familiar with, as shown in the following table:

Year Jan Feb March April May June
2016 20 108 45 10 105 48
2017 48 10 0 0 78 74
2018 12 102 10 0 0 100
2019 1 20 3 40 5 60

With a React Data Grid, the page code looks something like this:

import React from "react";
import ReactDataGrid from "react-data-grid";
import { Sparklines, SparklinesLine, SparklinesSpots } from "react-sparklines";

const Sparkline = ({ row }) => (
  <Sparklines
    data={[row.jan, row.feb, row.mar, row.apr, row.may, row.jun]}
    margin={6}
    height={40}
    width={200}
  >
    <SparklinesLine
      style=\{{ strokeWidth: 3, stroke: "#336aff", fill: "none" }}
    />
    <SparklinesSpots
      size={4}
      style=\{{ stroke: "#336aff", strokeWidth: 3, fill: "white" }}
    />
  </Sparklines>
);

const columns = [
  { key: "year", name: "Year" },
  { key: "jan", name: "January" },
  { key: "feb", name: "February" },
  { key: "mar", name: "March" },
  { key: "apr", name: "April" },
  { key: "may", name: "May" },
  { key: "jun", name: "June" },
  { name: "Info", formatter: Sparkline },
];

const rows = [
  { year: 2016, jan: 20, feb: 108, mar: 45, apr: 10, may: 105, jun: 48 },
  { year: 2017, jan: 48, feb: 10, mar: 0, apr: 0, may: 78, jun: 74 },
  { year: 2018, jan: 12, feb: 102, mar: 10, apr: 0, may: 0, jun: 100 },
  { year: 2019, jan: 1, feb: 20, mar: 3, apr: 40, may: 5, jun: 60 },
];

export default function ReactDataGridPage() {
  return (
    <ReactDataGrid
      columns={columns}
      rowGetter={(i) => rows[i]}
      rowsCount={rows.length}
    />
  );
}

For displaying charts and other graphics, I need to rely on third-party libraries. In the above case, I installed react-sparklines to demonstrate a sparkline. The columns are defined using an object. For the sparkline, I fall back to a custom formatter without a backing field.

The resulting grid looks as such:

Whats the Difference Between React Datagrids and Data Tables and Why Do You Need Them

Creating an Advanced React Data Grid

Now let's display the same data with FlexGrid. For about the same amount of code, you get a much better looking and more flexible display of data.

The page code now looks like this:

import "@grapecity/wijmo.styles/wijmo.css";
import React from "react";
import { CollectionView } from "@grapecity/wijmo";
import { FlexGrid, FlexGridColumn } from "@grapecity/wijmo.react.grid";
import { CellMaker, SparklineMarkers } from "@grapecity/wijmo.grid.cellmaker";
import { SortDescription } from "@grapecity/wijmo";

const data = [
  { year: 2016, jan: 20, feb: 108, mar: 45, apr: 10, may: 105, jun: 48 },
  { year: 2017, jan: 48, feb: 10, mar: 0, apr: 0, may: 78, jun: 74 },
  { year: 2018, jan: 12, feb: 102, mar: 10, apr: 0, may: 0, jun: 100 },
  { year: 2019, jan: 1, feb: 20, mar: 3, apr: 40, may: 5, jun: 60 },
];

export default function WijmoPage() {
  const [view] = React.useState(() => {
    const view = new CollectionView(
      data.map((item) => ({
        ...item,
        info: [item.jan, item.feb, item.mar, item.apr, item.may, item.jun],
      }))
    );
    return view;
  });

  const [infoCellTemplate] = React.useState(() =>
    CellMaker.makeSparkline({
      markers: SparklineMarkers.High | SparklineMarkers.Low,
      maxPoints: 25,
      label: "Info",
    })
  );

  return (
    <FlexGrid itemsSource={view}>
      <FlexGridColumn header="Year" binding="year" width="*" />
      <FlexGridColumn header="January" binding="jan" width="*" />
      <FlexGridColumn header="February" binding="feb" width="*" />
      <FlexGridColumn header="March" binding="mar" width="*" />
      <FlexGridColumn header="April" binding="apr" width="*" />
      <FlexGridColumn header="May" binding="may" width="*" />
      <FlexGridColumn header="June" binding="jun" width="*" />
      <FlexGridColumn
        header="Info"
        binding="info"
        align="center"
        width={180}
        allowSorting={false}
        cellTemplate={infoCellTemplate}
      />
    </FlexGrid>
  );
}

Most notably, the Wijmo data grid defines the columns declaratively in React. For the sparkline cells, we use Wijmo's CellMaker Sparkline control to insert a sparkline into each cell in the "Info" column. Using useState, I can cache the data and keep it alive between re-renderings - no expensive computation required.

Here, the default result has a look that resembles a real spreadsheet app:

Whats the Difference Between React Datagrids and Data Tables and Why Do You Need Them

Since the data grid is typically the largest component in your application, it's good practice to lazy-load it. If you'll only use the data grid on a single page, it's sufficient to lazy-load that particular page and avoid additional complexity:

import * as React from "react";
import { Switch, Route } from "react-router-dom";

const PageWithDatagrid = React.lazy(() => import("./pages/DatagridPage"));

export const Routes = () => (
  <Switch>
    {/* ... */}
    <Route path="/datagrid" component={PageWithDatagrid} />
  </Switch>
);

The only requirement is that the lazy-loaded module have a proper default export:

export default function PageWithDatagrid() {
  return /* ... */;
}

All unique dependencies (for instance, the data grid component) should be contained in the side-bundle. This side-bundle will have a significant impact on startup performance.

Ready to Create Custom React Applications? Download Wijmo Today!

Best Practices for Loading Data

In these examples, I simply loaded some hard-coded data. In real applications, you're most likely going to grab dynamic data from an external source like a file, a database, or an API.

While loading data is usually considered a mostly back-end topic, there are some front-end considerations that need to be discussed. Most importantly, having an API that delivers non-bounded amounts of data would be problematic. One common issue is that the rendering of the entire dataset is either really slow, or only happening in chunks, leaving parts of the data unused.

To circumvent the above issues, some APIs allow pagination. In the most simple form, you communicate a page number to the API, which then calculates the offset in the dataset. For reliable pagination and maximum flexibility, the pagination mechanism actually should use a pointer - a marker for the last emitted data item.

To include a paginated API in the Wijmo data grid, use a CollectionView instance. If your API supports OData, then you can simply use the ODataCollectionView for this task.

For instance, the following view serves six items per page:

const view = new ODataCollectionView(url, 'Customers', {
  pageSize: 6,
  pageOnServer: true,
  sortOnServer: true,
});

In general, the CollectionView can be used for asynchronous data loading too:

const [view, setView] = React.useState(() => new CollectionView());

React.useEffect(() => {
  fetch('https://jsonplaceholder.typicode.com/posts')
    .then(res => res.json())
    .then(posts => setView(view => {
      view.sourceCollection = data;
      return view;
    }));
}, []);

// render datagrid

The code above is not perfect: asynchronous operations should be appropriately cleaned up with a disposer. A better version of useEffect would be:

React.useEffect(() => { 
  const controller = new AbortController(); 
  const { signal } = controller;

  fetch('https://jsonplaceholder.typicode.com/posts', { signal }) 
    .then(res => res.json()) 
    .then(/* ... */);

  return () => controller.abort(); 
}, []);

Besides calling the API directly, you may be concerned with cross-origin resource sharing (CORS). CORS is a security mechanism in the browser that affects performing requests to domains other than the current one.

One crucial aspect besides the implicit CORS request and response pattern, including the so-called preflight requests, is the delivery of credentials by, for example, a cookie. By default, the credentials are only sent to same-origin requests.

The following will also deliver the credentials to other services - fi the service responded correctly to the preflight (OPTIONS) requests:

fetch('https://jsonplaceholder.typicode.com/posts', { credentials: 'include' })

So far, data calling has been done on mounting the component. This method is not ideal. It not only implies always needing to wait for the data, but also makes cancellations and other flows harder to implement.

What you want is some global data state, which could be easily (and independently of a particular component's lifecycle) accessed and changed. While state container solutions, such as Redux, are the most popular choices, there are simpler alternatives.

One possibility here is to use Zustand ("state" in German). You can model all data-related activities as manipulations on the globally defined state object. Changes to this object are reported via React hooks.

// state.js
import create from 'zustand';

const [useStore] = create(set => ({
  data: undefined,
  load: () =>
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(posts => set({ data: posts })),
}));

export { useStore };

// datagrid.js
import { useStore } from './state';
// ...

export default function MyDataGridPage() {
  const data = useStore(state => state.data);
  const load = useStore(state => state.load);
  const view = new CollectionView(data);

  React.useEffect(() => {
    if (!data) {
      load();
    }
  }, [data]);

  return (
    <FlexGrid itemsSource={view} />
  );
}

Next Steps:

Data grids have become incredibly popular and flexible tools for displaying, organizing, and even editing data in all kinds of applications. Using FlexGrid, you can build Angular, React, and Vue data grids quickly and easily - in under five minutes.

Ready to Try FlexGrid? Download Wijmo Today!

comments powered by Disqus