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"
}
}
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);
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>,
}),
];
// ...
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(),
});
//...
}
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>
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>
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
.
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>
);
};
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>
);
};
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>
);
};
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)
: '';
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))
: '';
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)
: '';
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()} />
),
}),
// ...
]
Working demo
Here is a working demo of this exercise.
Next: Virtualization
Top comments (0)