Click here to Skip to main content
15,868,016 members
Articles / Web Development / React
Article

React Data Grids - A Complete Guide

9 Sep 2020CPOL8 min read 19.6K   8   1
In this article, we'll learn how to integrate a data grid into React applications.
Here we'll look at a few best practices for integrating data into the grid, from importing files to connecting with APIs and databases.

This article is in the Product Showcase section for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers.

Image 1 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 heart, a simple table – a couple of columns and many rows. But even before the minimum viable application ships, it becomes clear that a simple table is not enough. The stakeholders want pagination and filtering. The designers demand personalization and flexibility. This is the moment when we (the developers) look for help in the form of an existing data grid.

In this article, we'll learn how to integrate a data grid into React applications and look at a few best practices for integrating data into the grid, from importing files to connecting with APIs and databases.

Data Grid Features

Data Grids vs. 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 feature we'll find in many data grids is a data export function. In the simplest case, this is equivalent to a clipboard copy. However, exports into a CSV file and even printed reports are not that different today.

In general, data grids support interoperability with standard spreadsheet applications such as Excel that 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 Power BI.

Features that truly distinguish data grids from tables are, for instance, custom cell renderings and format 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.

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 heart, we'll start with that. There are two essential ways to design a table in React:

  1. Following the typical HTML abstraction layer and creating components such as TableContainer using children: TableHeader, TableFooter, TableRow, and TableCell.
  2. Having 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 simplistically yet consistently styled table, the second option — a Table component with render props — is capable of much more by transitioning much of the representation logic into an abstraction layer. Therefore, it is the path usually taken in the existing solutions.

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

JavaScript
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 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:

JavaScript
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, there's very little reason to roll your own data grid.

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 some 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 GrapeCity's Wijmo 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.

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:

JavaScript
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 result shows up like this:

Image 2

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:

JavaScript
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 cell, a CollectionView is used. 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:

Image 3

Since the data grid is the largest component in the 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:

JavaScript
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:

JavaScript
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.

Best Practices for Loading Data

In these examples, I just 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 an ICollectionView 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:

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

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

JavaScript
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:

JavaScript
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 – if the service responded correctly to the preflight (OPTIONS) request:

JavaScript
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.

JavaScript
// 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} />
  );
}

Here is some additional info about React data grids:

  1. React Components Documentation
  2. React Components Demos

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
Germany Germany
Florian lives in Munich, Germany. He started his programming career with Perl. After programming C/C++ for some years he discovered his favorite programming language C#. He did work at Siemens as a programmer until he decided to study Physics.

During his studies he worked as an IT consultant for various companies. After graduating with a PhD in theoretical particle Physics he is working as a senior technical consultant in the field of home automation and IoT.

Florian has been giving lectures in C#, HTML5 with CSS3 and JavaScript, software design, and other topics. He is regularly giving talks at user groups, conferences, and companies. He is actively contributing to open-source projects. Florian is the maintainer of AngleSharp, a completely managed browser engine.

Comments and Discussions

 
BugMessage Closed Pin
30-Sep-20 0:17
Member 1495231330-Sep-20 0:17 
Answerit is good Pin
Member 149346899-Sep-20 21:19
Member 149346899-Sep-20 21:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.