Here we'll look at a few best practices for integrating data into the grid, from importing files to connecting with APIs and databases.
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:
- Following the typical HTML abstraction layer and creating components such as TableContainer using children: TableHeader, TableFooter, TableRow, and TableCell.
- 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:
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:
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:
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:
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 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:
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:
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.
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:
const view = new ODataCollectionView(url, 'Customers', {
pageSize: 6,
pageOnServer: true,
sortOnServer: true,
});
In general, standard 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;
}));
}, []);
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 – if the service responded correctly to the preflight (OPTIONS) request:
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.
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 };
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:
- React Components Documentation
- 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.