# rn-data-grid URL: /docs/native/docyrus/data-grid Headless spreadsheet grid for React Native — useDataGrid + DataGrid with FlashList virtualization, pinned columns, every cell variant, sheet editors, status transitions, grouping, change tracking, paging, color rules and gallery mode. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/rn-data-grid ``` **Dependencies:** - [@tanstack/react-table](https://www.npmjs.com/package/@tanstack/react-table) - [@shopify/flash-list](https://www.npmjs.com/package/@shopify/flash-list) - [react-native-svg](https://www.npmjs.com/package/react-native-svg) - [jsonata](https://www.npmjs.com/package/jsonata) - [@react-querybuilder/core](https://www.npmjs.com/package/@react-querybuilder/core) - [expo-clipboard (optional)](https://www.npmjs.com/package/expo-clipboard) The native grid mirrors the web API 1:1: a **headless controller** (`useDataGrid`) owns the TanStack table and all grid state, and `DataGrid` renders whatever it returns. Mobile idioms replace desktop ones — editors open in bottom sheets, long-press enters selection mode, long-press on a header opens the column actions, and rows are virtualized with FlashList v2. ## Usage ### Headless (recommended — same as web) ```tsx import { DataGrid, DataGridToolbar, getDataGridSelectColumn, useDataGrid, type ColumnDef } from '@/components/docyrus-native/data-grid'; type Employee = { id: string; name: string; status: string; salary: number; hireDate: string }; const columns: ColumnDef ); } ``` ### Convenience (`data` + `columns`, native-only) Without a `table` prop, `DataGrid` creates the controller internally. Every `useDataGrid` option is accepted, plus the legacy controlled `sorting` / `columnFilters` pairs and `onDataUpdate`. ```tsx ``` ### Interaction model | Gesture | Result | |---------|--------| | Tap a cell | Selection mode → toggle the row. Email / phone / url → contact sheet (Compose email · Call · Send message · Open link · Edit). Checkbox / switch → toggles in place. `tableMeta.onRowClick` set → called with the row. Editable cell → editor sheet. Otherwise → row detail sheet. | | Long-press a row | Selects it and enters selection mode (the selection bar appears). | | Tap a header | Cycles sort asc → desc → none. | | Long-press a header | Column actions: sort, pin / unpin, move left / right, group by, "Show autonumber" (relation), `tableMeta.columnHeaderActions(columnId)`, hide. | | Pull down | `onRefresh`. | ## useDataGrid ```ts function useDataGrid(options: UseDataGridProps): UseDataGridReturn; ``` ### Options Every TanStack `TableOptions` key (except `getCoreRowModel` / `pageCount`) is spread into the table — `data`, `columns`, `getRowId`, `state`, `initialState`, `onColumnVisibilityChange`, `onColumnOrderChange`, `onColumnPinningChange`, `onColumnSizingChange`, `onGroupingChange`, `onExpandedChange`, `onPaginationChange`, `manualPagination`, `manualSorting`, `manualFiltering`, `rowCount`, `enableRowSelection`, `meta`, … `sorting`, `columnFilters` and `rowSelection` are grid-owned (read `state.*` when supplied, else internal) and their `on*Change` callbacks receive the resolved value. | Option | Type | Default | Description | |--------|------|---------|-------------| | `data` | `TData[]` | — | Rows (required). | | `columns` | `ColumnDef[]` | — | Column definitions; `meta.cell` (`CellOpts`) picks the cell variant (required). | | `onDataChange` | `(data: TData[]) => void` | — | Receives the next data array after every cell write / discard. | | `onRowAdd` | `() => Partial \| Promise \| null \| void> \| null \| void` | — | Enables the "Add row" footer; the returned position is scrolled to. | | `onRowsAdd` | `(count: number) => void \| Promise` | — | Accepted for web parity. | | `onRowsDelete` | `(rows: TData[], rowIndices: number[]) => void \| Promise` | — | Enables the selection bar's Delete (through `tableMeta.onRowsDelete`). | | `onFilesUpload` | `(params: { files: NativeFile[]; rowIndex: number; columnId: string }) => Promise` | — | Makes `file` / `image` cells editable. | | `onFilesDelete` | `(params: { fileIds: string[]; rowIndex: number; columnId: string }) => void \| Promise` | — | File removal from the file editor. | | `rowHeight` | `RowHeightValue` | `'short'` | Initial row height (re-seeded when the prop changes). | | `onRowHeightChange` | `(rowHeight: RowHeightValue) => void` | — | Row height change notification. | | `displayMode` | `'table' \| 'gallery'` | `'table'` | Initial display mode (re-seeded when the prop changes). | | `onDisplayModeChange` | `(mode: DataGridDisplayMode) => void` | — | Display mode change notification. | | `enableSearch` | `boolean` | `false` | Find-in-grid (`searchState`); `DataGrid` renders the find bar. | | `enableGrouping` | `boolean` | `true` | Groupable variants get a normalized `getGroupingValue` (dates bucket by local day). | | `readOnly` | `boolean` | `false` | Disables every write. | | `enableChangeTracking` | `boolean` | `false` | Buffers edits: tinted cells + change bar with Save / Cancel. | | `onChangesSave` | `(changes: RowChange[], data: TData[]) => void \| Promise` | — | Save from the change bar. | | `onChangesDiscard` | `() => void` | — | Cancel from the change bar (data is restored through `onDataChange`). | | `getRowLabel` | `(row: TData, rowIndex: number) => string` | — | Row label for the change list and row detail title. | | `maxSearchMatches` | `number` | `500` | Find-in-grid match cap. | | `rowColorRules` | `DataGridRowColorRule[]` | — | JSONata row background rules (evaluated async). | | `cellColorRules` | `DataGridCellColorRule[]` | — | JSONata cell background rules. | | `pagingMode` | `'virtual-scroll' \| 'standard'` | `'virtual-scroll'` | `'standard'` = client pagination + paging footer. Switching at runtime re-seeds the page size. | | `pageSize` | `number` | `50` | Page size in `'standard'` mode. | | `overscan`, `measureRows`, `dir`, `autoFocus`, `enableSingleCellSelection`, `enableColumnSelection`, `enablePaste`, `onPaste`, `maxSelectCells` | — | — | Accepted and ignored (desktop-only). | ### Return value | Key | Type | Description | |-----|------|-------------| | `table` | `Table` | TanStack table (sorted / filtered / grouped / expanded / paginated models). | | `tableMeta` | `TableMeta` | Meta passed to cells (see below). | | `columns` | `ColumnDef[]` | The input columns. | | `rowHeight` | `RowHeightValue` | Current row height. | | `displayMode` | `DataGridDisplayMode` | Current display mode. | | `focusedCell` | `CellPosition \| null` | Last tapped / navigated cell. | | `editingCell` | `CellPosition \| null` | Cell whose editor sheet is open. | | `searchState` | `SearchState \| null` | `{ searchQuery, onSearchQueryChange, onSearch, searchMatches, matchIndex, searchOpen, onSearchOpenChange, onNavigateToNextMatch, onNavigateToPrevMatch }` when `enableSearch`. | | `onRowAdd` | `(() => Promise \| null>) \| undefined` | Wrapped `onRowAdd`. | | `changedCellsByRowId` | `Map> \| null` | Changed cells (change tracking only). | | `changedRowCount` | `number` | Rows with pending changes. | | `onChangesSave` | `(() => Promise) \| undefined` | Change tracking only. | | `onChangesDiscard` | `(() => void) \| undefined` | Change tracking only. | | `changeMapRef` | `RefObject>> \| undefined` | Pending change map. | | `getRowLabel` | `((row, rowIndex) => string) \| undefined` | Passed through. | | `rowColorMap` | `Map` | Row id → colour. | | `cellColorMap` | `Map>` | Row id → column id → colour. | | `pagingMode` | `DataGridPagingMode \| undefined` | Passed through. | | `selectedRows` | `TData[]` | Selected originals. | | `selectedRowCount` | `number` | Selected row count. | ### TableMeta `tableMeta` spreads `options.meta` and adds the grid-owned members. Supply the host-side members through `meta`. | Member | Type | Source | Description | |--------|------|--------|-------------| | `onDataUpdate` | `(update: CellUpdate \| CellUpdate[]) => void` | grid | Write one or many cells. | | `onRowsDelete` | `(rowIndices: number[]) => void \| Promise` | grid | Present when `onRowsDelete` option is set. | | `onRowSelect` | `(rowId: string, checked: boolean) => void` | grid | Toggle a row. | | `onCellClick` / `onCellEditingStart` / `onCellEditingStop` | functions | grid | Focus / open / close the editor sheet. | | `getIsCellChanged` | `(rowId, columnId) => boolean` | grid | Change tracking lookup. | | `getVisualRowIndex` | `(rowId) => number \| undefined` | grid | 1-based leaf index (skips group rows). | | `rowHeight` / `onRowHeightChange` | | grid | Read by `DataGridRowHeightMenu`. | | `displayMode` / `onDisplayModeChange` | | grid | Read by `DataGridDisplayMenu`. | | `focusedCell` / `editingCell` / `readOnly` | | grid | Current state. | | `onFilesUpload` / `onFilesDelete` | | option | File cells. | | `formatDate` / `formatDateTime` / `formatNumber` | formatters | `meta` › `DateFormatProvider` / `NumberFormatProvider` (e.g. `DocyrusTenantProvider`) › cell fallback (`toLocale*` / `Intl.NumberFormat`) | Tenant formatting. | | `onRowClick` | `(row: TData) => void` | `meta` | Row tap instead of the built-in detail sheet. | | `onStatusUpdate` | `(input: { rowIndex; columnId; value; secondaryValue?; description?; followupDate? }) => Promise` | `meta` | Enables the status note / follow-up step. | | `loadCellOptions` | `(column, { search, page, pageSize, signal }) => Promise<{ items; hasMore? }>` | `meta` | Async, paged, abortable options for relation / user / enum editors. | | `columnOptions` / `onColumnOptionsChange` / `onColumnOptionsReset` | | `meta` | Per-column overrides (`showAutonumber`). | | `columnHeaderActions` | `(columnId) => DataGridColumnHeaderAction[]` | `meta` | Extra header long-press actions. | | `onOpenRelation` / `getRelationHref` | `(args: RelationNavigationArgs) => void / string` | `meta` | Relation "open" affordance (`onOpenRelation` wins; the href opens with `Linking`). | | `emailClient` | `RestApiClient` | `meta` (`useDocyrusDataGrid` wires its `client`) | Email cell "Compose email" opens the grid-hosted `EmailComposeDialog` pre-addressed to the cell value (web parity). | | `messageClient` | `RestApiClient` | `meta` (`useDocyrusDataGrid` wires its `client`) | Phone cell "Send message" opens the grid-hosted `InstantMessageComposeDialog` (SMS / WhatsApp). | | `onComposeEmail` | `(address: string, row: TData) => void` | `meta` | **Native.** Handle the email action yourself (wins over `emailClient`; without either → `mailto:`). | | `onSendMessage` | `(phone: string, row: TData) => void` | `meta` | **Native.** Handle the message action yourself (wins over `messageClient`; without either → `sms:`). | ## DataGrid `DataGridProps = DataGridHeadlessProps | DataGridConvenienceProps`. ### Props (headless — spread `useDataGrid()`) | Prop | Type | Default | Description | |------|------|---------|-------------| | `table` | `Table` | — | From `useDataGrid`. Selects the headless path. | | `tableMeta` | `TableMeta` | — | From `useDataGrid`. | | `columns` | `ColumnDef[]` | — | From `useDataGrid`. | | `rowHeight` | `RowHeightValue` | — | From `useDataGrid` (36 / 56 / 76 / 96 dp, 1–4 lines). | | `displayMode` | `DataGridDisplayMode` | — | `'gallery'` renders `DataGridGallery`. | | `focusedCell` / `editingCell` | `CellPosition \| null` | — | From `useDataGrid`. | | `searchState` | `SearchState \| null` | — | Renders the find bar; matches are tinted and scrolled to. | | `onRowAdd` | `() => Promise \| null>` | — | "Add row" footer. | | `changedCellsByRowId`, `changedRowCount`, `onChangesSave`, `onChangesDiscard`, `changeMapRef` | | — | Change bar + tinted cells. | | `getRowLabel` | `(row, rowIndex) => string` | — | Change list + detail title. | | `rowColorMap` / `cellColorMap` | `Map` | — | Colour rules. | | `pagingMode` | `'virtual-scroll' \| 'standard'` | — | `'standard'` shows `DataGridPaginationFooter`. | | `actions` | `DataGridAction[]` | — | Selection bar actions. | | `onClearFilters` | `() => void` | resets column filters + search | Empty-state "Clear filters". | | `height` | `number \| 'auto'` | fill parent | Fixed height in dp. The grid is `flex-1` otherwise — inside a `ScrollView` (unbounded height) pass a number. | | `stretchColumns` | `boolean` | `false` | Grow columns to the viewport width. | | `addRowLabel` | `string` | `"Add row"` | Footer label. | | `cardConfig` | `DataGridCardConfig` | — | Legacy 3-slot card config (adapted). | | `galleryCardConfig` | `DataGalleryCardConfig` | — | Gallery card config (wins over `cardConfig`). | | `galleryDisplayConfig` | `DataGalleryDisplayConfig` | `DEFAULT_DATA_GALLERY_DISPLAY_CONFIG` | Gallery display config. | | `onCardClick` | `(record: TData, rowIndex: number) => void` | `onRowClick` / detail sheet | Gallery card press. | | `isLoading` | `boolean` | `false` | Skeleton while there are no rows. | | `isReloading` | `boolean` | `false` | Spinner overlay; rows stay mounted. | | `onRefresh` | `() => void` | — | Pull-to-refresh. | | `refreshing` | `boolean` | `isReloading` | Pull-to-refresh spinner. | | `emptyText` | `string` | `"No data"` / `"No results found"` | Empty-state title. | | `striped` | `boolean` | `false` | Alternate row tint. | | `showSummary` | `boolean` | `false` | Row count + display mode chips. | | `className` | `string` | — | Container classes. | | `selectedRows` / `selectedRowCount` | | — | Accepted and ignored (derived from `rowSelection`). | ### Props (convenience — no `table`) All `useDataGrid` options plus every view prop above, and: | Prop | Type | Default | Description | |------|------|---------|-------------| | `sorting` / `onSortingChange` | `SortingState` / `(sorting) => void` | — | Controlled sorting. | | `columnFilters` / `onColumnFiltersChange` | `ColumnFiltersState` / `(filters) => void` | — | Controlled filters (values are `DataGridFilterValue`). | | `onDataUpdate` | `(update: CellUpdate \| CellUpdate[]) => void` | — | Called for every cell write. | | `enableGrouping` | `boolean` | `false` | Legacy default (the hook defaults to `true`). | ### DataGridAction | Field | Type | Description | |-------|------|-------------| | `key` | `string` | Stable key (defaults to `label`). | | `label` | `string` | Button label. | | `icon` | `ReactNode` | Leading icon. | | `variant` | `'default' \| 'destructive'` | Button colour. | | `disabled` | `boolean` | Disable the button. | | `onAction` | `(selectedRows: TData[], rowIndices?: number[]) => void` | Press handler (web signature + native row indices). | | `render` | `(selectedRows: TData[]) => ReactNode` | Custom renderer (wins over the button). | ## Cell variants (`CellOpts`) | Variant | Options | Display | Editor | |---------|---------|---------|--------| | `short-text` / `long-text` | — | Text, clamped to the row-height line count | Text / textarea field | | `email` / `phone` / `url` | — | Link text | Contact sheet → Edit → email / phone / url field | | `number` / `currency` / `percent` | `min`, `max`, `step`, `decimalPrecision`, `thousandSeparator`, `currency`, `symbolPosition` | `tableMeta.formatNumber` or `Intl.NumberFormat` | Number / money / percent field | | `select` / `enum` | `options`, `display: 'badge' \| 'text'` (enum: `appSlug`, `dataSourceSlug`, `fieldSlug`) | Colour badge or dot + text | Searchable list (enum without options → `loadCellOptions`) | | `status` | `options` (`parent`, `isFinalOption`, `forceDescription`, `forceFollowupDate`), `display` | "Main › Sub" badges, final marker | Two-level list → note / follow-up step → `onStatusUpdate` | | `multi-select` / `tag-select` | `options` | Chips (+N) | Multi list with Save / Clear | | `user` / `user-multi-select` | `options` (`avatarUrl`, `initials`) | Avatar(s) + name (raw `{ id, firstname, lastname, photo }` supported) | Avatar list (async via `loadCellOptions` when no options) | | `relation` | `dataSourceId`, `relationAppSlug`, `relationDataSourceSlug`, `displayField`, `iconField`, `showAutonumber` | Icon / logo, autonumber, label, open affordance | Async paged list (`loadCellOptions`); writes `{ id, name }` | | `checkbox` / `switch` | `trueLabel`, `falseLabel` | Box / pill | Toggles in place | | `date` / `datetime` / `time` / `date-range` | — | `tableMeta.formatDate` / `formatDateTime` or locale fallback | Date / datetime / time / range pickers | | `rating` | `max`, `icon: RatingIconName` | Icons | Rating field | | `duration` | — | `HH:MM:SS` | Text input; `HH:MM` = hours:minutes (web parsing) | | `color` / `icon` / `currency-code` | — | Swatch / icon / code | Colour / icon / currency pickers | | `file` / `image` | `maxFileSize`, `maxFiles`, `accept`, `multiple` | File name / thumbnails | Upload / remove through `onFilesUpload` / `onFilesDelete` (read-only without them) | | `uuid` | `showCopyButton` | Mono text (long-press copies) or copy button — optional `expo-clipboard`, share-sheet fallback | — | | `chart` | `chartType: 'sparkline' \| 'bar'`, `dataKey`, `color`, `expandedCellContent` | SVG sparkline / bars (+ expand sheet) | — | | `sparkline` | `color`, `dataKey` | Deprecated alias of `chart` | — | Columns whose `header` is a function (e.g. `getDataGridSelectColumn`, `getDataGridActionsColumn`) render through `flexRender`, like web. Column width is the TanStack `size` (falls back to the deprecated `meta.width`). ## Toolbar & menus Every menu is a compact chip that opens a bottom sheet (web: popover / dropdown). All take `table` and the common props below unless noted. | Common prop | Type | Default | Description | |-------------|------|---------|-------------| | `table` | `Table` | — | The grid's TanStack table. | | `disabled` | `boolean` | `false` | Disable the trigger. | | `className` | `string` | — | Trigger chip classes. | | Component | Extra props | Description | |-----------|-------------|-------------| | `DataGridToolbar` | `enableFilter` (`true`), `enableSort` (`true`), `enableRowHeight` (`true`), `enableFields` (`true`), `enableView` (`true`), `enableDisplayMode` (`false`), `enableGroup` (`false`), `startContent`, `endContent`, `contentClassName`; `enableSearch` / `enablePaste` / `enableRowAdd` / `enableRowsDelete` / `enableUndoRedo` accepted and ignored | Horizontally scrollable row of the menus. | | `DataGridFilterMenu` | `getAsyncOptions?: (column) => AsyncOptionsConfig \| undefined` | Column filter chips (operator + value). | | `DataGridAdvancedFilter` | `group: RuleGroupType \| undefined`, `onChange: (group \| undefined) => void` | AND/OR query-builder draft editor, emits on Apply. | | `DataGridSortMenu` | — | Multi-column sort. | | `DataGridGroupMenu` | `defaultRowGroupingColumn?: string` | Row grouping. | | `DataGridRowHeightMenu` | — | `short` / `medium` / `tall` / `extra-tall`. | | `DataGridFieldsMenu` | `enableReorder` (`true`), `enablePinning` (`true`) | Visibility, order, pin-left. | | `DataGridDisplayMenu` | — | Table ↔ gallery. | | `DataGridViewMenu` | `storageKey?: string` | Local saved views (sync `'local'` store). | | `DataGridExportMenu` | `onExport?: (rows, format, columnScope) => void`, `isExporting`, `fileName` (`'export'`) | CSV / Excel / JSON / Markdown; built-in writer + share sheet without `onExport`. | | `DataGridSearch` | `searchQuery`, `onSearchQueryChange`, `onSearch?`, `searchMatches?`, `matchIndex?`, `onNavigateToNextMatch?`, `onNavigateToPrevMatch?`, `searchOpen?`, `onSearchOpenChange?`, `placeholder?`, `autoFocus?` (no `table`) | Find-in-grid bar. | ### DataGridSidePanel Container for `useDocyrusDataGrid`'s `vertical-tabs` view picker (`sidePanel`). Web renders a fixed `