This episode enriches TanStack Table with enterprise features: column visibility and grouping, aggregation with sum functions, row selection and expansion, virtualized tables, and custom cell rendering for dynamic data displays.

Episode 6 built a basic table. Now we move into the features that separate a simple table from an enterprise datagrid: hideable columns, grouping with aggregation, row selection, expansion, and custom cell rendering.
Episode 9 covers column visibility, grouping and aggregation, row selection and expansion, virtualized tables, and custom cell rendering. All these features are enabled by adding row models and state — not by rewriting logic.
This is also the first episode where TanStack Table and TanStack Virtual work together, opening the door to tables with hundreds of thousands of rows.
Column visibility is controlled by the columnVisibility state. Toggling a column is just calling column.toggleVisibility:
const [columnVisibility, setColumnVisibility] = useState({})
const table = useReactTable({
data,
columns,
state: { columnVisibility },
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
})state and onColumnVisibilityChange make visibility a controlled state. You can render a checkbox per column that calls column.toggleVisibility(), letting users control which columns appear without changing the column definitions.
Grouping groups rows by the value of a specific column. Enable it with getGroupedRowModel, then call column.getToggleGroupingHandler() on the header:
const table = useReactTable({
data,
columns,
state: { grouping },
onGroupingChange: setGrouping,
getCoreRowModel: getCoreRowModel(),
getGroupedRowModel: getGroupedRowModel(),
getExpandedRowModel: getExpandedRowModel(),
})getGroupedRowModel() turns the data into group rows, and getExpandedRowModel() manages which groups are open. Groups can be opened and closed with row.getIsExpanded() and row.getToggleExpandedHandler().
When data is grouped, numeric columns often need aggregation. TanStack Table ships built-in aggregationFn functions like sum, min, max, and count:
columnHelper.accessor("gaji", {
header: "Gaji",
aggregationFn: "sum",
aggregatedCell: ({ getValue }) => `Total: ${getValue()}`,
})aggregationFn: "sum" computes the sum of values within each group. aggregatedCell decides how the aggregate value is rendered — here as Total. In group rows this column shows the sum; in detail rows it shows the original value.
Row selection is enabled with enableRowSelection. Add a checkbox column using columnHelper.display:
columnHelper.display({
id: "pilih",
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
})columnHelper.display creates a column with no data — specifically for actions like checkboxes. row.getIsSelected() and row.getToggleSelectedHandler() manage each row's state; table.getIsAllRowsSelected() manages select-all.
For large data, combine TanStack Table with TanStack Virtual. The rendered rows are replaced with virtual items:
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
const virtualizer = useVirtualizer({
count: table.getRowModel().rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
})
const rows = table.getRowModel().rows
const virtualRows = virtualizer.getVirtualItems()virtualizer.getVirtualItems() returns only the visible rows. When rendering, virtual rows are positioned absolutely with translateY, and the container's total height is set to virtualizer.getTotalSize(). The full virtualization details are covered in episode 11.
The cell in a column definition can be a function that returns any element. This lets tables support badges, buttons, links, even small charts:
columnHelper.accessor("status", {
header: "Status",
cell: ({ getValue }) => {
const status = getValue()
return (
<span className={status === "aktif" ? "badge-hijau" : "badge-merah"}>
{status}
</span>
)
},
})cell: ({ getValue }) => ... receives context and returns a React element. Because rendering is entirely yours, table design is limited only by your imagination — not by the library's API.
Info
Combine a virtualized table with getRowCanExpand for expanding rows that contain nested tables or forms. Episode 23 uses this combination in a full application.
Episode 9 enriched your table: column visibility with controlled state, grouping with aggregation, row selection through checkboxes, virtualized tables for massive data, and custom cell rendering for free-form displays.
Key takeaways:
In the next episode, episode 10, we'll discuss router advanced and data management — deferred data loading with Await, error boundaries and notFound, route-based mutations with router invalidation, and full-stack routing patterns with data loaders. Your router will learn to handle heavy data and errors!