DEV Community

Cover image for Let's create Data Table. Part 2: Add TanStack table and cell formatters
Dima Vyshniakov
Dima Vyshniakov

Posted on β€’ Edited on

1 1 1

Let's create Data Table. Part 2: Add TanStack table and cell formatters

This is an article from the series about creating of an advanced Data table component using React, TanStack Table 8, Tailwind CSS and Headless UI.

In the previous article, we created an HTML skeleton of the Data table. This time, we will use it to render table data.

Our exercise starts with the useReactTable hook from TanStack. To use it, we have to provide table data and columns configuration. So let's do it.

Mocking data

Our table requires a data to display within its cells. TanStack expects this data to be an array of Rows, where each row adheres to the same type defined for the table. Columns can then access and display this data. When the data reference changes, the table will automatically rerender to reflect the updated data.

{
  "firstName": "Bunni",
  "lastName": "Evanne",
  "randomDecimal": -230804.30489955097,
  "dateExample": "2021-08-27T10:27:07.994Z",
  "email": "Waino.Ratke@gmail.lol",
  "address": {
    "city": "New Aryannabury",
    "country": "KH",
    "streetAddress": "898 Murazik Mission",
    "phoneNumber": "+49-322-0322372"
  },
  "business": {
    "iban": "FR49600377403775574738YOU09",
    "companyName": "Condemned Amethyst Mouse"
  }
}
Enter fullscreen mode Exit fullscreen mode

To effectively test our component, we need to utilize substantial datasets. For this purpose, we will use a custom function, src/mocks/generateData.ts. This function, which has already been implemented for this demonstration, will generate the necessary data.

To ensure consistent randomized results during testing, we will provide a unique seed value as an input. Additionally, we will specify the desired number of rows for the generated dataset. generateData function will then produce the required data based on these inputs.

import { Row } from '../DataTable/types.ts';
import { generateData } from './mocks/generateData.ts';

const SEED = 66;

const ROWS_AMOUNT = 100;

const tableData: Row[] = generateData(ROWS_AMOUNT, SEED);
Enter fullscreen mode Exit fullscreen mode

Column config

Next thing, we have to define our table columns configuration. It's done inside src/DataTable/columnsConfig.tsx. We have to provide an array of column configurations. Here we use the createColumnHelper function from TanStack and set the same type as used for data creation. Next, we create accessor column (a column which displays a piece of data we provided). The first parameter defines an accessor key to extract data from the Row. The second is config object. Here are the official docs. Inside the config object, we define header and cell components for the table.

import { createColumnHelper } from '@tanstack/react-table';
import { Row } from './types.ts';

const columnHelper = createColumnHelper<Row>();

export const columns = [
  columnHelper.accessor('firstName', {
    header: () => <div>First name</div>,
    cell: (props) => <div>{props.getValue()}</div>,
  }),
];
// ...
Enter fullscreen mode Exit fullscreen mode

Table component

Here is how our table component code looks now.

Apply useReactTable hook

import { FC } from 'react';
import classNames from 'classnames';
import {
  useReactTable,
  getCoreRowModel,
  flexRender,
} from '@tanstack/react-table';
import { columns } from './columnsConfig.tsx';
import { Row } from './types.ts';

type Props = {
  tableData: Row[];
};

export const DataTable: FC<Props> = ({ tableData }) => {
  const table = useReactTable({
    columns,
    data: tableData,
    getCoreRowModel: getCoreRowModel(),
  });
  //...
}
Enter fullscreen mode Exit fullscreen mode

Implement header row

The provided code defines a thead element for a table with a sticky header. It uses the table.getHeaderGroups() method to generate header groups and their respective headers. Each header is rendered within a th element, with specific styles applied using the classNames utility.

<thead className="sticky left-0 top-0 z-20">
  {table.getHeaderGroups().map((headerGroup) => (
    <tr key={headerGroup.id}>
      {headerGroup.headers.map((header) => {
        return (
          <th
            key={header.id}
            className={classNames(
              // Tailwind classes
            )}
          >
            {header.isPlaceholder
              ? null
              : flexRender(
                  header.column.columnDef.header,
                  header.getContext(),
                )}
          </th>
        );
      })}
    </tr>
  ))}
</thead>
Enter fullscreen mode Exit fullscreen mode

Add table body implementation.

The provided code defines a tbody element for a table, which dynamically generates rows and cells based on the data from the table object. It uses the table.getRowModel().rows method to iterate over each row and the row.getVisibleCells() method to iterate over each cell within a row. Each cell is rendered within a td element, with specific styles applied using the classNames utility. The content of the td element is rendered using the flexRender function, which takes cell.column.columnDef.cell and cell.getContext() as arguments to render the cell content.

<tbody>
  {table.getRowModel().rows.map((row) => (
    <tr key={row.id}>
      {row.getVisibleCells().map((cell) => (
        <td
          key={cell.id}
          className={classNames(
            // Tailwind classes
          )}
        >
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
        </td>
      ))}
    </tr>
  ))}
</tbody>
Enter fullscreen mode Exit fullscreen mode

Different cell types

We also have the requirement to apply correct data formatting to different types of table cells. We put everything related to cell into the src/DataTable/cells folder. These will be β€œdumb” React components only capable of styling the content properly. Let's try to keep Data table logic contained in src/DataTable/columnsConfig.tsx.

Various cell formats

We have six different cell types in total for this table. Each cell React component should be capable of consuming the data from the corresponding table Row (e.g., "2021-08-27T10:27:07.994Z" or "KH") and render them properly formatted to help user to make decisions or build a narrative using this table.

We have to provide a workaround to reset HTMLTableElement rendering context and use block context instead. To achieve this, we have to set width for each cell React component.

Header cell

Here is the Header cell. This component is used to render a table header cell with a specified title and column width. It takes a title prop representing the title of the column, and columnWidth representing the width of the column in pixels. The latter property has to be taken from column context like this: header: props => <Cell columnWidth={props.column.getSize()} />

import { FC } from 'react';

export type Props = {
  title: string;
  columnWidth: number;
};

export const HeaderCell: FC<Props> = 
({ title, columnWidth }) => {
  return (
    <div
      className="p-1.5 font-semibold" 
      style={{ width: columnWidth }}
    >
      {title}
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

Text cell

Text cell is similar to Header cell. Except, we apply Tailwind truncate class to it. This tells the browser to truncate overflowing text with ellipsis if needed. We also set the title attribute to reveal the complete cell value if needed.

import { FC } from 'react';

export type Props = {
  value?: string;
  columnWidth: number;
};

export const TextCell: FC<Props> = 
({ value, columnWidth }) => {
  return (
    <div
      className="truncate p-1.5"
      title={value}
      style={{ width: columnWidth }}
    >
      {value}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Numeric cell

As you may notice, we display numbers formatted according to en-US locale requirements. Number cell component uses Intl.NumberFormat built-in object to achieve this.

We also use tabular-nums Tailwind CSS class, which tells the browser to render numbers in the special format designed to perceive large amounts of data. We also align text right to match format requirements.

fractionDigits property allows to defined number of digits to display for each number. Extra digits are rounded, missing are replaced by zero.

We provide the locale value as a string with a BCP 47 language tag. Number formatting is done by Intl.NumberFormat a standard browser built-in object.

import { FC } from 'react';

export type Props = {
  value?: number;
  fractionDigits?: number;
  columnWidth: number;
};

const LOCALE = 'en-US';

export const NumberCell: FC<Props> = ({
  value,
  fractionDigits = 0,
  columnWidth,
}) => {
  const formattedValue =
    value !== undefined
      ? new Intl.NumberFormat(LOCALE, {
          style: 'decimal',
          minimumFractionDigits: fractionDigits,
          maximumFractionDigits: fractionDigits,
        }).format(value)
      : '';

  return (
    <div
      className="truncate p-1.5 text-right tabular-nums"
      title={formattedValue}
      style={{ width: columnWidth }}
    >
      {formattedValue}
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

Currency cell

This cell is very similar to numeric. The different configuration of Intl.NumberFormat is done. Amount of fraction digits is hardcoded to 2; style is set to currency. value is ISO 4217 currency code.

const formattedValue =
    value !== undefined
      ? new Intl.NumberFormat(LOCALE, {
          style: 'currency',
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
          currency,
        }).format(value)
      : '';
Enter fullscreen mode Exit fullscreen mode

Date cell

Date cell uses Intl.DateTimeFormat browser built-in object to format. value is an ISO string (e.g., new Date().toISOString()).

  const formattedValue =
    value !== undefined
      ? new Intl.DateTimeFormat(LOCALE, {
          year: 'numeric',
          month: 'short',
          weekday: 'short',
          day: 'numeric',
        }).format(new Date(value))
      : '';
Enter fullscreen mode Exit fullscreen mode

Country name cell

This cell looks similar to a Text cell but uses Intl.DisplayNames built-in object to format data. value is a two-letter ISO 3166 region code.

const formattedValue =
    value !== undefined
      ? new Intl.DisplayNames(LOCALE, { type: 'region' }).of(value)
      : '';
Enter fullscreen mode Exit fullscreen mode

Columns config with sizes and components

This is how they are implemented in src/DataTable/columnsConfig.tsx. Now, we set a size property to each cell component using Column context (i.e., props parameter) as columnWidth={props.column.getSize()}. We can set this size manually via the size property as a number of pixels or agree with the 150 default.


import { createColumnHelper } from '@tanstack/react-table';
import { Row } from './types.ts';
import { HeaderCell } from './cells/HeaderCell.tsx';
import { TextCell } from './cells/TextCell.tsx';

const columnHelper = createColumnHelper<Row>();

export const columns = [
  columnHelper.accessor('firstName', {
    size: 120,
    header: (props) => {
      return (
        <HeaderCell title="First name" columnWidth={props.column.getSize()} />
      );
    },
    cell: (props) => (
      <TextCell columnWidth={props.column.getSize()} value={props.getValue()} />
    ),
  }),
  columnHelper.accessor('lastName', {
    // size of 150 is used by default
    // size: 150,
    header: (props) => {
      return (
        <HeaderCell title="Last name" columnWidth={props.column.getSize()} />
      );
    },
    cell: (props) => (
      <TextCell columnWidth={props.column.getSize()} value={props.getValue()} />
    ),
  }),
 // ...
]

Enter fullscreen mode Exit fullscreen mode

Working demo

Here is a working demo of this exercise.

Next: Virtualization

πŸ’‘ One last tip before you go

DEV++

Spend less on your side projects

We have created a membership program that helps cap your costs so you can build and experiment for less. And we currently have early-bird pricing which makes it an even better value! πŸ₯

Check out DEV++

Top comments (0)