unity-data-table
Load when rendering tabular data with Unity. Use it to choose DataTable for managed table state or primitive Table for static/custom markup, including filtering, pagination, virtualization, or bulk actions.
This skill targets the current workspace Unity v3 API. Legacy examples are migration context.
DataTable is the composite (Tanstack Table + Unity Table + pagination + empty states + virtualization). Table is the primitive — use only when you have no table state to manage.
Setup
Minimum working client-side DataTable. Keep columns and data references stable across renders. Define static columns outside the component or memoize them; memoize derived data when its source would otherwise create a new array.
import { useMemo } from 'react'
import {
DataTable,
DataTableRoot,
TableCell,
TableRow,
} from '@payfit/unity-components'
import {
createColumnHelper,
createPaginatedRowModel,
flexRender,
rowPaginationFeature,
tableFeatures,
useTable,
} from '@tanstack/react-table'
type Employee = {
id: string
name: string
position: string
status: 'active' | 'inactive'
}
const features = tableFeatures({
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
})
const columnHelper = createColumnHelper<typeof features, Employee>()
export function EmployeeTable({ employees }: { employees: Employee[] }) {
const columns = useMemo(
() => [
columnHelper.accessor('name', {
header: 'Name',
meta: { isRowHeader: true, headerClassName: 'uy:w-1/3' },
}),
columnHelper.accessor('position', { header: 'Position' }),
columnHelper.accessor('status', { header: 'Status' }),
],
[],
)
const data = useMemo(() => employees, [employees])
const table = useTable({
features,
data,
columns,
})
return (
<DataTableRoot>
<DataTable table={table} layout="fixed">
{row => (
<TableRow key={row.id}>
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)}
</DataTable>
</DataTableRoot>
)
}Core Patterns
- Define columns with createColumnHelper and use ColumnMeta for row headers, focus behavior, helper text, and fixed-layout widths.
- For server pagination, pass only the current page, set manualPagination, and derive pageCount from the server total.
- Map FilterToolbar.onChange into TanStack Table's column/global filter state.
- Put DataTableBulkActions beside DataTable inside DataTableRoot and drive it from row-selection state.
Read references/patterns.md when implementing one of these patterns.
Common Mistakes
CRITICAL Define columns inline (not memoized)
Wrong:
function MyTable() {
const columns = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'status', header: 'Status' },
]
const table = useTable({ features, data, columns, ... })
return <DataTable table={table}>{row => ...}</DataTable>
}Correct:
function MyTable() {
const columns = useMemo(() => [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'status', header: 'Status' },
], [])
const table = useTable({ features, data, columns, ... })
return <DataTable table={table}>{row => ...}</DataTable>
}A new columns array each render makes Tanstack Table re-run the whole pipeline; rows re-render even when data is unchanged.
HIGH Return string from cell function instead of JSX
Wrong:
{ accessorKey: 'status', cell: info => info.getValue() }Correct:
{
accessorKey: 'status',
cell: info => <Badge color={statusColor(info.getValue())}>{info.getValue()}</Badge>
}flexRender expects ReactNode. A raw string renders unstyled and may overflow. Cell layout is the maintainer-flagged hotspot for table questions.
HIGH Use the primitive Table when DataTable + Tanstack would do
Wrong:
<Table layout="fixed">
<TableHeader>…</TableHeader>
<TableBody>
{data.map(row => (
<TableRow>…</TableRow>
))}
</TableBody>
</Table>Correct:
const features = tableFeatures({
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
})
const table = useTable({ features, data, columns })
<DataTableRoot>
<DataTable table={table}>{row => /* ... */}</DataTable>
</DataTableRoot>Primitive Table has no state; agent hand-rolls pagination/sorting/selection that DataTable provides out of the box.
HIGH Manage filter state separately from FilterToolbar
Wrong:
const [filters, setFilters] = useState([])
<FilterToolbar filterDefs={…} onChange={setFilters} />
<DataTable table={table} />Correct:
const [columnFilters, setColumnFilters] = useState([])
const [globalFilter, setGlobalFilter] = useState('')
const features = tableFeatures({
columnFilteringFeature,
globalFilteringFeature,
filteredRowModel: createFilteredRowModel(),
})
const table = useTable({ features, data, columns })
<FilterToolbar filterDefs={…} onChange={mapToTableState(setColumnFilters, setGlobalFilter)} />FilterToolbar.onChange emits AppliedFilter[]. Agent stores them locally without mapping to table.setGlobalFilter / setColumnFilters, so rows do not actually filter.
HIGH Pass every local row with manual pagination
Wrong:
useTable({
features: tableFeatures({ rowPaginationFeature }),
data: allEmployees,
manualPagination: true,
rowCount: allEmployees.length,
})Correct for locally held data:
import { useMemo, useState } from 'react'
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })
const currentData = useMemo(() => {
const start = pagination.pageIndex * pagination.pageSize
return allEmployees.slice(start, start + pagination.pageSize)
}, [pagination])
useTable({
features: tableFeatures({ rowPaginationFeature }),
data: currentData,
manualPagination: true,
rowCount: allEmployees.length,
state: { pagination },
onPaginationChange: setPagination,
})manualPagination: true tells Tanstack not to slice. Agents pass the full dataset and expect TanStack to paginate anyway.
For server-side pagination, fetch and pass only the current server page and set rowCount or pageCount from the server response. When all rows are local and TanStack should paginate them automatically, omit manualPagination and register paginatedRowModel: createPaginatedRowModel() in tableFeatures.
MEDIUM Render large datasets without enableVirtualization
Wrong:
<DataTable table={table}>{row => …}</DataTable>Correct:
<DataTable
table={table}
enableVirtualization
estimatedRowHeight={40}
overscan={10}
>
{row => …}
</DataTable>Without virtualization, 500+ rows mount in the DOM. Keyboard nav context clones cells; reconciliation is O(n) per render.