@payfit/unity-components
Version:
274 lines (220 loc) • 7.63 kB
Markdown
name: unity-data-table
description: >
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.
metadata:
type: core
library: '@payfit/unity-components'
library_version: '2.x'
sources:
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/data-table/DataTable.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/data-table/parts/DataTableRoot.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/data-table/parts/DataTableBulkActions.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/table/Table.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/filter-toolbar/FilterToolbar.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/filter/Filter.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/docs/guides/data/Building Tables.mdx'
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.
```tsx
import { useMemo, useState } from 'react'
import {
DataTable,
DataTableRoot,
TableCell,
TableRow,
} from '@payfit/unity-components'
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from '@tanstack/react-table'
type Employee = {
id: string
name: string
position: string
status: 'active' | 'inactive'
}
const columnHelper = createColumnHelper<Employee>()
export function EmployeeTable({ employees }: { employees: Employee[] }) {
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })
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 = useReactTable({
data,
columns,
getRowId: row => row.id,
state: { pagination },
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
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](references/patterns.md) when implementing one of
these patterns.
## Common Mistakes
### CRITICAL Define columns inline (not memoized)
Wrong:
```tsx
function MyTable() {
const columns = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'status', header: 'Status' },
]
const table = useReactTable({ data, columns, ... })
return <DataTable table={table}>{row => ...}</DataTable>
}
```
Correct:
```tsx
function MyTable() {
const columns = useMemo(() => [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'status', header: 'Status' },
], [])
const table = useReactTable({ 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:
```tsx
{ accessorKey: 'status', cell: info => info.getValue() }
```
Correct:
```tsx
{
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:
```tsx
<Table layout="fixed">
<TableHeader>…</TableHeader>
<TableBody>
{data.map(row => (
<TableRow>…</TableRow>
))}
</TableBody>
</Table>
```
Correct:
```tsx
const table = useReactTable({ data, columns, getCoreRowModel(),
getPaginationRowModel(), state: { pagination }, onPaginationChange: setPagination })
<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:
```tsx
const [filters, setFilters] = useState([])
<FilterToolbar filterDefs={…} onChange={setFilters} />
<DataTable table={table} />
```
Correct:
```tsx
const [columnFilters, setColumnFilters] = useState([])
const [globalFilter, setGlobalFilter] = useState('')
const table = useReactTable({ /* … */ state: { columnFilters, globalFilter },
onGlobalFilterChange: setGlobalFilter, onColumnFiltersChange: setColumnFilters,
getFilteredRowModel() })
<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:
```tsx
useReactTable({
data: allEmployees,
manualPagination: true,
pageCount: Math.ceil(allEmployees.length / 10),
})
```
Correct for locally held data:
```tsx
const currentData = useMemo(() => {
const start = pagination.pageIndex * pagination.pageSize
return allEmployees.slice(start, start + pagination.pageSize)
}, [pagination])
useReactTable({
data: currentData,
manualPagination: true,
pageCount: Math.ceil(allEmployees.length / pagination.pageSize),
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 `pageCount` from the server's total count. When all rows are local and
TanStack should paginate them automatically, omit `manualPagination` and use
`getPaginationRowModel()` instead.
### MEDIUM Render large datasets without enableVirtualization
Wrong:
```tsx
<DataTable table={table}>{row => …}</DataTable>
```
Correct:
```tsx
<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.