Data Table
Headless TanStack table renderer for React Native — FlashList body, sticky header, frozen columns, row grouping, skeleton loading and a pagination footer, with a data/columns convenience path.
Installation
pnpm dlx @docyrus/cli add @docyrus/rn-data-tablepnpm add @tanstack/react-table @shopify/flash-listUsage
Headless (web API)
Like the web DataTable, the native component renders a TanStack table instance you build elsewhere — typically with useDocyrusDataTable, or directly with useReactTable.
import {
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
type ColumnDef
} from '@tanstack/react-table';
import { DataTable, getDataTableSelectColumn } from '@/components/docyrus-native/data-table';
const columns: ColumnDef<Person>[] = [
getDataTableSelectColumn<Person>(),
{ accessorKey: 'name', header: 'Name', size: 160 },
{ accessorKey: 'role', header: 'Role' },
{ accessorKey: 'status', header: 'Status' }
];
function PeopleTable({ people, isLoading }: { people: Person[]; isLoading: boolean }) {
const table = useReactTable({
data: people,
columns,
getRowId: row => row.id,
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel()
});
return (
<DataTable
table={table}
pagination
isLoading={isLoading}
maxHeight={480}
onRowClick={row => router.push(`/people/${row.original.id}`)} />
);
}Convenience path
Without table, DataTable builds the instance itself from data + columns and the enable* flags / controlled state pairs.
<DataTable
data={people}
columns={columns}
enableSorting
enableRowSelection
enableMultiRowSelection
enableGrouping
grouping={['role']}
pagination={{ enabled: true, pageSize: 10 }}
maxHeight={400} />Layout notes
- The table card needs a bounded height: pass
maxHeight, or render it in a parent with a fixed / flex height. The body is a FlashList (always virtualized). - The header stays visible while the body scrolls (
stickyHeader, defaulttrue) and uses a muted background. - Columns pinned left (
columnPinning.left) — plus the reservedselectandactionscolumns, which are always frozen on the leading edge — stay in place while the table scrolls horizontally.columnPinning.rightcolumns freeze on the trailing edge. - When the columns are narrower than the screen, the scrolling (non-pinned) columns stretch to fill it.
- Grouped rows render as full-width group headers (chevron, value visual — avatar / image / icon / colour dot — label and row count). Tap a header to expand / collapse. Labels come from
resolveGroupHeaderPresentation(the same helper as the web data grid), andmeta.renderGroupValueis honoured except for date buckets. - Tap a row →
onRowClick(row). Long-press a row → toggles its selection (when the row can be selected). Interactive children (checkboxes, links, buttons) keep their own presses.
API Reference
DataTable — shared props
| Prop | Type | Default | Description |
|---|---|---|---|
table | Table<TData> | — | External TanStack table instance (headless mode). When set, the convenience props below are not accepted. |
className | string | — | Outer wrapper (table card + pagination footer) |
containerClassName | string | — | The bordered table card |
tableClassName | string | — | The horizontally scrolling content (header + rows) |
headerClassName | string | — | Header row (also applied to pinned header cells) |
bodyClassName | string | — | The FlashList body wrapper |
rowClassName | string | ((row: Row<TData>) => string | undefined) | — | Per-row classes |
cellClassName | string | — | Applied to every body cell, in addition to columnDef.meta.cellClassName |
emptyText | string | 'No results.' | Empty-state text (ui.dataTable.noResults) |
isLoading | boolean | false | Render skeleton rows instead of data |
loadingText | string | — | Accessible label and caption under the skeleton rows |
pagination | boolean | DataTableLegacyPagination | false | true renders a footer driven by table.getState().pagination (honours manualPagination + rowCount). The legacy object is accepted by the convenience path only. |
pageSizeOptions | ReadonlyArray<number> | DATA_GRID_PAGE_SIZE_OPTIONS | Page-size choices in the footer's page-size sheet |
onRowClick | (row: Row<TData>) => void | — | Fired when a non-group row is pressed |
size | 'sm' | 'md' | 'lg' | 'md' | Cell padding and text size |
variant | 'default' | 'outlined' | 'elevated' | 'default' | Card style |
zebra | boolean | false | Alternate row backgrounds |
rowDividers | boolean | true | Divider line between rows |
columnDividers | boolean | false | Divider line between columns |
stickyHeader | boolean | true | Keep the header visible while the body scrolls. false scrolls it away with the rows. |
maxHeight | number | — | Maximum height of the table card |
maxCellCharacters | number | — | Truncate plain-text cell content after N characters |
skeletonRowCount | number | 8 | Number of skeleton rows while isLoading |
testID | string | — | Test identifier on the outer wrapper |
DataTable — convenience props (no table)
| Prop | Type | Default | Description |
|---|---|---|---|
data | TData[] | — | Table data (required on this path) |
columns | ColumnDef<TData>[] | — | Column definitions (required on this path) |
getRowId | (row: TData) => string | row.id, else the index | Row id accessor (selection / pinning state keys) |
getSubRows | (row: TData) => TData[] | undefined | row.subRows | Sub-row accessor, used when enableExpanding |
enableSorting | boolean | true | Tap a header to cycle its sort |
enableFiltering | boolean | false | Apply columnFilters |
enableGlobalFilter | boolean | false | Apply globalFilter |
enableColumnVisibility | boolean | — | Informational; visibility follows columnVisibility |
enableRowSelection | boolean | false | Prepends a pinned getDataTableSelectColumn() (unless columns already has a select column); a row press also toggles selection |
enableMultiRowSelection | boolean | false | Allow selecting several rows |
enableColumnPinning | boolean | false | Honour columnPinning |
enableColumnResizing | boolean | — | Accepted for compatibility; no-op on touch |
enableGrouping | boolean | false | Honour grouping. Columns without getGroupingValue get a stable key for object values, so expanded enum / relation objects no longer collapse into one [object Object] group. |
enableExpanding | boolean | false | Expandable sub-rows (chevron + indentation in the first data column) |
enableRowPinning | boolean | false | Honour rowPinning (pinned rows render at the top / bottom with an accent rule) |
sorting / onSortingChange | SortingState / OnChangeFn<SortingState> | — | Controlled sorting |
columnFilters / onColumnFiltersChange | ColumnFiltersState / OnChangeFn<ColumnFiltersState> | — | Controlled column filters |
globalFilter / onGlobalFilterChange | string / OnChangeFn<string> | — | Controlled global filter |
columnVisibility / onColumnVisibilityChange | VisibilityState / OnChangeFn<VisibilityState> | — | Controlled visibility (grouped columns are hidden automatically) |
rowSelection / onRowSelectionChange | RowSelectionState / OnChangeFn<RowSelectionState> | — | Controlled selection |
expanded / onExpandedChange | ExpandedState / OnChangeFn<ExpandedState> | — | Controlled expanded rows / groups |
columnPinning / onColumnPinningChange | ColumnPinningState / OnChangeFn<ColumnPinningState> | — | Controlled column pinning |
grouping / onGroupingChange | GroupingState / OnChangeFn<GroupingState> | — | Controlled grouping |
rowPinning / onRowPinningChange | RowPinningState / OnChangeFn<RowPinningState> | — | Controlled row pinning |
Controlled on*Change handlers always receive the resolved next value (never an updater function).
DataTableLegacyPagination
| Prop | Type | Default | Description |
|---|---|---|---|
enabled | boolean | — | Enable pagination |
pageSize | number | 10 | Rows per page (client paging) |
pageIndex | number | 0 | Initial page index (zero-based) |
currentPage | number | — | Current page (one-based) for server paging |
itemsPerPage | number | — | Page size for server paging |
totalItems | number | — | Total row count — enables server paging (no client slicing) |
showFirstLast | boolean | true | Show first / last buttons |
showPrevNext | boolean | — | Ignored — previous / next are always shown |
showPageNumbers | boolean | true | Show page-number buttons (otherwise page / total) |
maxPageNumbers | number | 5 | Maximum visible page numbers |
onPaginationChange | (pageIndex: number, pageSize: number) => void | — | Page change callback |
getDataTableSelectColumn
getDataTableSelectColumn<TData>(options?) returns the reserved select column (header checkbox toggles all page rows; cell checkbox toggles the row). DataTable always freezes it on the leading edge.
| Option | Type | Default | Description |
|---|---|---|---|
enableRowNumbers | boolean | false | Show the 1-based row number until the row is selected (tap the number to select) |
size | number | 44 | Column width |
enableHiding | boolean | false | Allow hiding the column |
enableSorting | boolean | false | Allow sorting the column |
| …rest | Partial<ColumnDef<TData>> | — | Any other column option except id / header / cell |
DataTablePagination
Presentational footer (also used by the convenience path's legacy pagination).
| Prop | Type | Default | Description |
|---|---|---|---|
currentPage | number | — | 1-based current page |
totalPages | number | — | Page count |
onPageChange | (page: number) => void | — | Receives the 1-based target page |
startItem / endItem / totalItems | number | — | Range summary ({start}–{end} of {total}) |
pageSize | number | — | Current page size (shows the page-size picker with onPageSizeChange) |
pageSizeOptions | ReadonlyArray<number> | DATA_GRID_PAGE_SIZE_OPTIONS | Page-size choices |
onPageSizeChange | (pageSize: number) => void | — | Page-size change handler |
showFirstLast | boolean | true | Show first / last buttons |
showPageNumbers | boolean | true | Page-number buttons (otherwise page / total) |
maxPageNumbers | number | 5 | Maximum visible page numbers |
alwaysShow | boolean | false | Render even when there is a single page |
size | 'sm' | 'md' | 'lg' | 'md' | Button / text size |
className | string | — | Container classes |
DataTablePaginationFooter
<DataTablePaginationFooter table={table} /> renders DataTablePagination from the table's own pagination state (getRowCount(), setPageIndex, setPageSize). Props: table, size?, pageSizeOptions?, className?. This is what pagination renders.
DataTableToolbar
Native-only convenience toolbar for the data/columns path. Headless tables should use the data-grid toolbar menus.
| Prop | Type | Default | Description |
|---|---|---|---|
globalFilter | string | — | Current search value |
onGlobalFilterChange | (value: string) => void | — | Shows the search input when set |
searchPlaceholder | string | 'Search...' | Search placeholder (ui.common.searchPlaceholder) |
enableColumnVisibility | boolean | — | Shows the Fields button (opens a checklist sheet) |
columns | { id: string; label: string; visible: boolean }[] | — | Column visibility list |
onColumnVisibilityChange | (columnId: string, visible: boolean) => void | — | Visibility change handler |
actions | ReactNode | — | Custom trailing actions |
className | string | — | Container classes |
Translation keys
ui.dataTable.noResults, ui.dataTable.loading, ui.dataTable.expandGroup, ui.dataTable.collapseGroup, ui.dataTable.ungrouped, ui.dataGrid.pagination.{empty,range,pageSize,first,prev,pageOf,next,last}, ui.dataGrid.fields, ui.common.searchPlaceholder.
Breaking changes (native major)
styles,headerStyle,bodyStyle,rowStyle,cellStyle,toolbarStyle(allany) were removed — useclassName,containerClassName,tableClassName,headerClassName,bodyClassName,rowClassNameandcellClassName.onRowClicknow receives the TanStackRow<TData>(web parity) instead of the raw record: userow.original.stickyHeadernow defaults totrue, and the body is always a FlashList (the non-virtualized ScrollView path and the "mobile cards" fallback were removed).DataTablePropsno longer extendsViewProps(no...restspread onto the container);testIDis still supported.data/columnsare no longer required whentableis passed;DataTablePropsis now a union ofDataTableHeadlessPropsandDataTableConvenienceProps.- With
enableRowSelection, the selection checkbox is now a realselectcolumn (getDataTableSelectColumn) instead of an extra 50px cell, and a long-press toggles selection. - Expanding uses TanStack's expanded row model (
getSubRows, keyed by row id) instead of renderingoriginal.subRowsmanually. - Legacy
pagination.showPrevNextis ignored.
Components
| Component | Description |
|---|---|
DataTable | Headless table renderer + convenience path |
DataTablePagination | Presentational pagination footer |
DataTablePaginationFooter | Pagination footer bound to a table's state |
DataTableToolbar | Search / fields / actions toolbar (convenience path) |
getDataTableSelectColumn | Factory for the reserved select column |
Type Exports
| Type | Description |
|---|---|
DataTableProps | DataTableHeadlessProps | DataTableConvenienceProps |
DataTableBaseProps | Props shared by both paths |
DataTableHeadlessProps | Props with an external table |
DataTableConvenienceProps | Props for the data + columns path |
DataTableLegacyPagination | Legacy pagination object |
DataTablePaginationProps | Props for DataTablePagination |
GetDataTableSelectColumnOptions | Options for getDataTableSelectColumn |
DataTableToolbarProps | Props for DataTableToolbar |
ColumnDef | Re-exported from @tanstack/react-table |
DataImportWizard
Five-step spreadsheet import wizard (Upload → Map fields → Options → Preview → Result) in a full-height bottom sheet. The file is picked with the system document picker and parsed on the server. API-aligned with the web DataImportWizard.
DataTableFilter
Typed filter builder with 7 data types, ~70 operators including relative dates, async option loading, faceted counts and a mobile step wizard.