iTranslated by AI
How to Dynamically Generate Table Columns with TanStack Table v8
What is TanStack Table?
TanStack Table is a headless table library that can be used with frameworks such as React, Vue, Solid, and Svelte.
Key Features:
- 🎨 Headless: UI-agnostic, allowing integration with any CSS framework or component library.
- 📦 Lightweight: Tree-shaking support allows you to import only the features you need.
- 🔧 Type-safe: Written in TypeScript, providing excellent type inference.
- 🚀 High Performance: Optimized through virtualization and memoization.
💡 shadcn/ui's Data Table also uses TanStack Table under the hood.
Introduction (What this article covers)
In business systems, it is common to implement tables where the number and types of data change dynamically.
In this article, we will explain how to implement a table that meets the following requirements using TanStack Table (React Table) v8.
- ✅ Dynamic Column Generation: The number of columns changes according to the number of data groups.
- ✅ Group Headers: Display related columns grouped together.
- ✅ TypeScript Support: Type-safe implementation.
Environment and Prerequisites
Technology Stack
| Technology | Version | Purpose |
|---|---|---|
| React | 18.x | UI Library |
| TypeScript | 5.x | Type System |
| @tanstack/react-table | 8.x | Table Library |
Installation
# npm
npm install @tanstack/react-table
# yarn
yarn add @tanstack/react-table
# pnpm
pnpm add @tanstack/react-table
Implementation Points
1. Dynamic Type Definitions
In TanStack Table, it is recommended to strictly define data types. However, when columns increase or decrease dynamically, you can use index signatures.
// Data type with dynamic keys
type DynamicRowData = {
[key: string]: string | number | undefined;
};
// Specify the type in createColumnHelper
const columnHelper = createColumnHelper<DynamicRowData>();
2. Creating a Column Helper Function
Prepare a helper function to write column definitions concisely.
const createColumn = (id: string, header: string, isNumeric = false) => {
return columnHelper.accessor(id, {
cell: (info) => info.getValue(),
header: header,
meta: isNumeric ? { isNumeric: true } : undefined,
});
};
Leveraging the meta Property
TanStack Table's meta can store arbitrary custom data. Here, we set an isNumeric flag, which can be used to apply styles such as right-alignment during rendering.
3. Dynamic Column Generation (useMemo)
Column definitions are memoized using useMemo and are recalculated only when the dependent data changes.
type Props = {
mainData: RowData[];
subGroups: RowData[][]; // Data groups that increase or decrease dynamically
};
const columns = useMemo(() => {
if (!mainData || mainData.length === 0) {
return [];
}
const cols: ColumnDef<DynamicRowData, string>[] = [];
// 1️⃣ Fixed columns
cols.push(
columnHelper.group({
id: "info",
header: "",
columns: [
createColumn("name", "Name"),
createColumn("code", "Code"),
],
}),
);
// 2️⃣ Main data columns
cols.push(
columnHelper.group({
id: "total",
header: "Total",
columns: [
createColumn("count0", "Count", true),
createColumn("amount0", "Amount", true),
],
}),
);
// 3️⃣ Sub-group columns (dynamically generated)
for (let i = 0; i < subGroups.length; i++) {
cols.push(
columnHelper.group({
id: `group${i + 1}`,
header: `Group ${i + 1}`,
columns: [
createColumn(`count${i + 1}`, "Count", true),
createColumn(`amount${i + 1}`, "Amount", true),
],
}),
);
}
return cols;
}, [mainData, subGroups]);
4. Implementing Group Headers
Using columnHelper.group() allows you to group multiple columns together.
columnHelper.group({
id: "group1", // Unique ID
header: "Group 1", // Group header label
columns: [ // Columns included in the group
createColumn("count1", "Count", true),
createColumn("amount1", "Amount", true),
],
});
Image of the rendering result:
┌──────────┬──────────┬────────────────┬────────────────┬────────────────┐
│ │ │ Total │ Group 1 │ Group 2 │
├──────────┼──────────┼────────┬───────┼────────┬───────┼────────┬───────┤
│ Name │ Code │ Count │ Amount│ Count │ Amount│ Count │ Amount│
├──────────┼──────────┼────────┼───────┼────────┼───────┼────────┼───────┤
│ ... │ ... │ ... │ ... │ ... │ ... │ ... │ ... │
5. Data Transformation Process
Convert the original data into a flat format suitable for table display.
type RowData = {
id: number;
name: string;
code: string;
count: number;
amount: number;
};
const data = useMemo(() => {
if (!mainData || mainData.length === 0) {
return [];
}
// Combine main data and subgroups
const allGroups: RowData[][] = [mainData, ...subGroups];
// Aggregate data using ID as the key
const aggregated: Record<number, DynamicRowData> = {};
allGroups.forEach((group, groupIndex) => {
group.forEach((item) => {
if (!(item.id in aggregated)) {
aggregated[item.id] = {
id: item.id,
name: item.name,
code: item.code,
};
}
// Set values to the columns for each group
aggregated[item.id][`count${groupIndex}`] = item.count;
aggregated[item.id][`amount${groupIndex}`] = item.amount;
});
});
return Object.values(aggregated);
}, [mainData, subGroups]);
6. Table Rendering
Create a table instance using the useReactTable hook and render cells with flexRender.
import {
useReactTable,
getCoreRowModel,
flexRender,
} from "@tanstack/react-table";
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
});
return (
<table>
{/* Header rendering */}
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} colSpan={header.colSpan}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
{/* Body rendering */}
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
Full Sample Code
Dynamic Column Table Component (Click to expand)
import { useMemo } from "react";
import {
ColumnDef,
createColumnHelper,
useReactTable,
getCoreRowModel,
flexRender,
} from "@tanstack/react-table";
// Data type with dynamic keys
type DynamicRowData = {
[key: string]: string | number | undefined;
};
// Original data type
type RowData = {
id: number;
name: string;
code: string;
count: number;
amount: number;
};
const columnHelper = createColumnHelper<DynamicRowData>();
type Props = {
mainData: RowData[];
subGroups: RowData[][];
};
export const DynamicTable = ({ mainData, subGroups }: Props) => {
// Column generation helper
const createColumn = (id: string, header: string, isNumeric = false) => {
return columnHelper.accessor(id, {
cell: (info) => info.getValue(),
header: header,
meta: isNumeric ? { isNumeric: true } : undefined,
});
};
// Dynamic column definition
const columns = useMemo(() => {
if (!mainData?.length) return [];
const cols: ColumnDef<DynamicRowData, string>[] = [];
// Fixed columns
cols.push(
columnHelper.group({
id: "info",
header: "",
columns: [
createColumn("name", "Name"),
createColumn("code", "Code"),
],
}),
);
// Total columns
cols.push(
columnHelper.group({
id: "total",
header: "Total",
columns: [
createColumn("count0", "Count", true),
createColumn("amount0", "Amount", true),
],
}),
);
// Sub-group columns (dynamic)
subGroups.forEach((_, i) => {
cols.push(
columnHelper.group({
id: `group${i + 1}`,
header: `Group ${i + 1}`,
columns: [
createColumn(`count${i + 1}`, "Count", true),
createColumn(`amount${i + 1}`, "Amount", true),
],
}),
);
});
return cols;
}, [mainData, subGroups]);
// Data transformation
const data = useMemo(() => {
if (!mainData?.length) return [];
const allGroups: RowData[][] = [mainData, ...subGroups];
const aggregated: Record<number, DynamicRowData> = {};
allGroups.forEach((group, groupIndex) => {
group.forEach((item) => {
if (!(item.id in aggregated)) {
aggregated[item.id] = {
id: item.id,
name: item.name,
code: item.code,
};
}
aggregated[item.id][`count${groupIndex}`] = item.count;
aggregated[item.id][`amount${groupIndex}`] = item.amount;
});
});
return Object.values(aggregated);
}, [mainData, subGroups]);
// Table instance
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
});
if (data.length === 0) {
return <p>No data available</p>;
}
return (
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} colSpan={header.colSpan}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
Tips & Best Practices
1. Performance Optimization
// ❌ Bad: Generates a new array every time
const columns = data.map((_, i) => createColumn(`col${i}`, `Column ${i}`));
// ✅ Good: Memoize with useMemo
const columns = useMemo(
() => data.map((_, i) => createColumn(`col${i}`, `Column ${i}`)),
[data]
);
2. Utilizing Hidden Columns
You can keep data for internal management in the table while hiding it from the display.
const table = useReactTable({
// ...
state: {
columnVisibility: {
id: false, // ID (Hidden)
sortOrder: false, // Sort Order (Hidden)
},
},
});
3. Number Formatting
Apply number formatting according to the locale.
const formatNumber = (value: number) => {
return value.toLocaleString("ja-JP", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
};
// Use in cell definition
columnHelper.accessor("amount", {
cell: (info) => formatNumber(info.getValue() as number),
header: "Amount",
});
4. Type-Safe Meta Definition
In TanStack Table v8, the meta type can be extended.
// types.d.ts
import "@tanstack/react-table";
declare module "@tanstack/react-table" {
interface ColumnMeta<TData extends RowData, TValue> {
isNumeric?: boolean;
align?: "left" | "center" | "right";
}
}
5. Proper Handling of Empty Data
This is how to handle cases where data is missing between groups.
// Set default values during data conversion
allGroups.forEach((group, groupIndex) => {
group.forEach((item) => {
// ...
aggregated[item.id][`count${groupIndex}`] = item.count ?? 0;
aggregated[item.id][`amount${groupIndex}`] = item.amount ?? 0;
});
});
Summary
Here is a summary of the key points for dynamic column generation using TanStack Table v8.
| Item | Implementation Method |
|---|---|
| Dynamic Type Definition | Index Signature [key: string]: T
|
| Column Generation |
useMemo + createColumnHelper
|
| Group Headers | columnHelper.group() |
| Hidden Columns | Controlled by columnVisibility
|
TanStack Table is a highly flexible and powerful library that can handle complex requirements like dynamic column generation. Its official documentation is extensive, so be sure to check it for further details.
Alternative Approach
In this article, we introduced a pattern for handling "group arrays that increase or decrease dynamically," but another approach is to have group information within the data itself and transform it using Object.groupBy.
The following article explains a data-driven method for column generation:
Choose the approach that best fits your specific use case.
Discussion