Docyrus

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.

iOSAndroid
Preview Data Table on your device

Scan with Expo Go

Download Expo Go, then scan the QR code to preview native components.

Installation

pnpm dlx @docyrus/cli add @docyrus/rn-data-table
Required Packages(2 packages)
pnpm add @tanstack/react-table @shopify/flash-list

Usage

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, default true) and uses a muted background.
  • Columns pinned left (columnPinning.left) — plus the reserved select and actions columns, which are always frozen on the leading edge — stay in place while the table scrolls horizontally. columnPinning.right columns 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), and meta.renderGroupValue is 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

PropTypeDefaultDescription
tableTable<TData>—External TanStack table instance (headless mode). When set, the convenience props below are not accepted.
classNamestring—Outer wrapper (table card + pagination footer)
containerClassNamestring—The bordered table card
tableClassNamestring—The horizontally scrolling content (header + rows)
headerClassNamestring—Header row (also applied to pinned header cells)
bodyClassNamestring—The FlashList body wrapper
rowClassNamestring | ((row: Row<TData>) => string | undefined)—Per-row classes
cellClassNamestring—Applied to every body cell, in addition to columnDef.meta.cellClassName
emptyTextstring'No results.'Empty-state text (ui.dataTable.noResults)
isLoadingbooleanfalseRender skeleton rows instead of data
loadingTextstring—Accessible label and caption under the skeleton rows
paginationboolean | DataTableLegacyPaginationfalsetrue renders a footer driven by table.getState().pagination (honours manualPagination + rowCount). The legacy object is accepted by the convenience path only.
pageSizeOptionsReadonlyArray<number>DATA_GRID_PAGE_SIZE_OPTIONSPage-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
zebrabooleanfalseAlternate row backgrounds
rowDividersbooleantrueDivider line between rows
columnDividersbooleanfalseDivider line between columns
stickyHeaderbooleantrueKeep the header visible while the body scrolls. false scrolls it away with the rows.
maxHeightnumber—Maximum height of the table card
maxCellCharactersnumber—Truncate plain-text cell content after N characters
skeletonRowCountnumber8Number of skeleton rows while isLoading
testIDstring—Test identifier on the outer wrapper

DataTable — convenience props (no table)

PropTypeDefaultDescription
dataTData[]—Table data (required on this path)
columnsColumnDef<TData>[]—Column definitions (required on this path)
getRowId(row: TData) => stringrow.id, else the indexRow id accessor (selection / pinning state keys)
getSubRows(row: TData) => TData[] | undefinedrow.subRowsSub-row accessor, used when enableExpanding
enableSortingbooleantrueTap a header to cycle its sort
enableFilteringbooleanfalseApply columnFilters
enableGlobalFilterbooleanfalseApply globalFilter
enableColumnVisibilityboolean—Informational; visibility follows columnVisibility
enableRowSelectionbooleanfalsePrepends a pinned getDataTableSelectColumn() (unless columns already has a select column); a row press also toggles selection
enableMultiRowSelectionbooleanfalseAllow selecting several rows
enableColumnPinningbooleanfalseHonour columnPinning
enableColumnResizingboolean—Accepted for compatibility; no-op on touch
enableGroupingbooleanfalseHonour grouping. Columns without getGroupingValue get a stable key for object values, so expanded enum / relation objects no longer collapse into one [object Object] group.
enableExpandingbooleanfalseExpandable sub-rows (chevron + indentation in the first data column)
enableRowPinningbooleanfalseHonour rowPinning (pinned rows render at the top / bottom with an accent rule)
sorting / onSortingChangeSortingState / OnChangeFn<SortingState>—Controlled sorting
columnFilters / onColumnFiltersChangeColumnFiltersState / OnChangeFn<ColumnFiltersState>—Controlled column filters
globalFilter / onGlobalFilterChangestring / OnChangeFn<string>—Controlled global filter
columnVisibility / onColumnVisibilityChangeVisibilityState / OnChangeFn<VisibilityState>—Controlled visibility (grouped columns are hidden automatically)
rowSelection / onRowSelectionChangeRowSelectionState / OnChangeFn<RowSelectionState>—Controlled selection
expanded / onExpandedChangeExpandedState / OnChangeFn<ExpandedState>—Controlled expanded rows / groups
columnPinning / onColumnPinningChangeColumnPinningState / OnChangeFn<ColumnPinningState>—Controlled column pinning
grouping / onGroupingChangeGroupingState / OnChangeFn<GroupingState>—Controlled grouping
rowPinning / onRowPinningChangeRowPinningState / OnChangeFn<RowPinningState>—Controlled row pinning

Controlled on*Change handlers always receive the resolved next value (never an updater function).

DataTableLegacyPagination

PropTypeDefaultDescription
enabledboolean—Enable pagination
pageSizenumber10Rows per page (client paging)
pageIndexnumber0Initial page index (zero-based)
currentPagenumber—Current page (one-based) for server paging
itemsPerPagenumber—Page size for server paging
totalItemsnumber—Total row count — enables server paging (no client slicing)
showFirstLastbooleantrueShow first / last buttons
showPrevNextboolean—Ignored — previous / next are always shown
showPageNumbersbooleantrueShow page-number buttons (otherwise page / total)
maxPageNumbersnumber5Maximum 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.

OptionTypeDefaultDescription
enableRowNumbersbooleanfalseShow the 1-based row number until the row is selected (tap the number to select)
sizenumber44Column width
enableHidingbooleanfalseAllow hiding the column
enableSortingbooleanfalseAllow sorting the column
…restPartial<ColumnDef<TData>>—Any other column option except id / header / cell

DataTablePagination

Presentational footer (also used by the convenience path's legacy pagination).

PropTypeDefaultDescription
currentPagenumber—1-based current page
totalPagesnumber—Page count
onPageChange(page: number) => void—Receives the 1-based target page
startItem / endItem / totalItemsnumber—Range summary ({start}–{end} of {total})
pageSizenumber—Current page size (shows the page-size picker with onPageSizeChange)
pageSizeOptionsReadonlyArray<number>DATA_GRID_PAGE_SIZE_OPTIONSPage-size choices
onPageSizeChange(pageSize: number) => void—Page-size change handler
showFirstLastbooleantrueShow first / last buttons
showPageNumbersbooleantruePage-number buttons (otherwise page / total)
maxPageNumbersnumber5Maximum visible page numbers
alwaysShowbooleanfalseRender even when there is a single page
size'sm' | 'md' | 'lg''md'Button / text size
classNamestring—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.

PropTypeDefaultDescription
globalFilterstring—Current search value
onGlobalFilterChange(value: string) => void—Shows the search input when set
searchPlaceholderstring'Search...'Search placeholder (ui.common.searchPlaceholder)
enableColumnVisibilityboolean—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
actionsReactNode—Custom trailing actions
classNamestring—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 (all any) were removed — use className, containerClassName, tableClassName, headerClassName, bodyClassName, rowClassName and cellClassName.
  • onRowClick now receives the TanStack Row<TData> (web parity) instead of the raw record: use row.original.
  • stickyHeader now defaults to true, and the body is always a FlashList (the non-virtualized ScrollView path and the "mobile cards" fallback were removed).
  • DataTableProps no longer extends ViewProps (no ...rest spread onto the container); testID is still supported.
  • data / columns are no longer required when table is passed; DataTableProps is now a union of DataTableHeadlessProps and DataTableConvenienceProps.
  • With enableRowSelection, the selection checkbox is now a real select column (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 rendering original.subRows manually.
  • Legacy pagination.showPrevNext is ignored.

Components

ComponentDescription
DataTableHeadless table renderer + convenience path
DataTablePaginationPresentational pagination footer
DataTablePaginationFooterPagination footer bound to a table's state
DataTableToolbarSearch / fields / actions toolbar (convenience path)
getDataTableSelectColumnFactory for the reserved select column

Type Exports

TypeDescription
DataTablePropsDataTableHeadlessProps | DataTableConvenienceProps
DataTableBasePropsProps shared by both paths
DataTableHeadlessPropsProps with an external table
DataTableConveniencePropsProps for the data + columns path
DataTableLegacyPaginationLegacy pagination object
DataTablePaginationPropsProps for DataTablePagination
GetDataTableSelectColumnOptionsOptions for getDataTableSelectColumn
DataTableToolbarPropsProps for DataTableToolbar
ColumnDefRe-exported from @tanstack/react-table

On this page