---
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.
