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