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

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

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

Our exercise starts with useReactTable hook from TanStack. In order 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

Our domain requires us to test the component with lengthy chunks of data. So we have to use a custom function generateData. I already created one for this demo. So let's assume it exists and works like this.

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

// unique seed to keep randomized results consistent
const SEED = 66;

// desired rows amount
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.

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

// Here we set the same type we used for data creation.
const columnHelper = createColumnHelper<Row>();

export const columns = [
  /**
   * Create an accessor column.
   * The first parameter defines an accessor key to extract data from the Row.
   * The second is config object.
   * @see https://tanstack.com/table/v8/docs/guide/column-defs
   */
  columnHelper.accessor('firstName', {
    // Provide a header cell component for the column
    header: () => <div>First name</div>,
    // Provide a body cell component for the column
    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 = {
  /**
   * Provide a data to render
   */
  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

<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(
              // basic styles
              'whitespace-nowrap bg-stone-600 text-left font-normal text-gray-100',
              // border styles
              'border-t border-solid border-t-stone-600 border-b border-b-stone-600 border-r border-r-stone-300 first:border-l first:border-l-stone-300',
            )}
          >
            {header.isPlaceholder
              ? null
              : flexRender(
                  header.column.columnDef.header,
                  header.getContext(),
                )}
          </th>
        );
      })}
    </tr>
  ))}
</thead>
Enter fullscreen mode Exit fullscreen mode

Add table body implementation.

<tbody>
  {table.getRowModel().rows.map((row) => (
    <tr key={row.id}>
      {row.getVisibleCells().map((cell) => (
        <td
          key={cell.id}
          className={classNames(
            // basic styles
            'whitespace-nowrap font-normal text-gray-700',
            // border styles
            'border-b border-solid border-b-stone-300 border-r border-r-stone-300 first:border-l first:border-l-stone-300',
          )}
        >
          {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 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 to consume 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. In order to achieve this, we have to set width for each cell React component.

Header cell

Here is the Header cell. We implemented table context workaround by having columnWidth prop

import { FC } from 'react';

export type Props = {
  /**
   * Provide the title for the column
   */
  title: string;
  /**
   * Set the width of a column in pixels
   * @example
   * { header: props => <Cell columnWidth={props.column.getSize()} /> }
   */
  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 = {
  /**
   * Provide the value to render in the cell
   */
  value?: string;
  /**
   * Set the width of a column in pixels
   * @example
   * { header: props => <Cell columnWidth={props.column.getSize()} /> }
   */
  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 amount of digits to display for each number. Extra digits are rounded, missing are replaced by zero.

import { FC } from 'react';

export type Props = {
  /**
   * Provide the value to render in the cell
   */
  value?: number;
  /**
   * Provide the number of fraction digits to show.
   * The displayed number will be rounded or zeroes will be attached if needed.
   */
  fractionDigits?: number;
  /**
   * Set the width of a column in pixels
   * @example
   * { header: props => <Cell columnWidth={props.column.getSize()} /> }
   */
  columnWidth: number;
};

/**
 * Provide a string with a BCP 47 language tag or an Intl.Locale instance,
 * or an array of such locale identifiers.
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#locales
 */
const LOCALE = 'en-US';

export const NumberCell: FC<Props> = ({
  value,
  fractionDigits = 0,
  columnWidth,
}) => {
  /**
   * Intl.NumberFormat is a standard browser built-in object
   * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
   */
  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 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 similat to Text cell but uses Intl.DisplayNames built-in object to format data. value is 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 param) as columnWidth={props.column.getSize()}. We can set this size manually via size property as a number of pixels or agree with 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 working demo of this exercise.

Next: Virtualization

Top comments (0)