UNPKG

@payfit/unity-components

Version:

274 lines (220 loc) 7.63 kB
--- 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.