UNPKG

@tanstack/table-core

Version:

Headless UI for building powerful tables & datagrids for TS/JS.

175 lines (116 loc) 6.77 kB
--- name: row-selection description: > Maintain rowSelection ID state with stable getRowId, single, multi, subrow, and Shift-range rules, selected row models, handler anchors, and manual-pagination semantics. Load when implementing getToggleSelectedHandler, enableRowRangeSelection, selectChildren, deselectParents, or selected IDs that outlive loaded Row objects. metadata: { type: sub-skill, library: '@tanstack/table-core', library_version: '9.0.0' } requires: ['core', 'table-features'] sources: - 'TanStack/table:docs/framework/react/guide/row-selection.md' - 'TanStack/table:packages/table-core/src/features/row-selection' - 'TanStack/table:examples/react/row-selection' --- This skill builds on `core` and `table-features`. Selection is independent ID state; selected row models can only materialize loaded rows. ## Setup ```ts import { rowSelectionFeature, tableFeatures, type Row, } from '@tanstack/table-core' type Person = { id: string; name: string } export const features = tableFeatures({ rowSelectionFeature }) export const options = { getRowId: (row: Person) => row.id, enableSubRowSelection: false, } ``` ## Core Patterns ```ts const selectedIds = table.getSelectedRowIds() const loadedSelectedRows = table.getSelectedRowModel().rows ``` Use IDs for database-wide intent and row models for currently loaded objects. ### Inclusive Shift ranges through the row handler ```ts export function getSelectionHandler(row: Row<typeof features, Person>) { return row.getToggleSelectedHandler() } ``` The handler establishes a table-local anchor on ordinary interactions and applies the checked value to the inclusive current display-order range on Shift interactions. Range behavior is enabled by default; set `enableRowRangeSelection: false` to preserve non-range handler behavior. Direct `row.toggleSelected()` and `table.setRowSelection()` calls do not move that anchor. Pass the original checkbox click event to this handler. DOM `change` events often omit modifier keys, so use the framework's click binding for row checkboxes unless its change event exposes the original click through `nativeEvent` (as React does). ### Limit a range to explicitly displayed rows ```ts export function getDisplayedRowsOnlyHandler(row: Row<typeof features, Person>) { return row.getToggleSelectedHandler({ selectChildren: false }) } ``` The default `selectChildren: true` recursively changes selectable descendants of parents encountered in the range. Set it to `false` when collapsed descendants outside the display-order interval must remain unchanged. ### Prune stale parent ids on child deselection ```ts export function getPruningHandler(row: Row<typeof features, Person>) { return row.getToggleSelectedHandler({ deselectParents: true }) } ``` Selecting a parent cascades its id plus selectable descendant ids into state, but deselecting a child later leaves the parent id behind by default (some tables treat state ids as literal selections, e.g. with `selectChildren: false`). The default `deselectParents: false` preserves that; set it to `true` so deselecting any row also deletes every ancestor id, keeping `row.getIsSelected()` honest for parents. Applies to `toggleSelected` and both plain and Shift-range handler paths. ### Select-all honors sub-row selection rules With `enableSubRowSelection: false` (or a per-row predicate), `table.toggleAllRowsSelected()` skips descendants of blocking parents, and `getIsAllRowsSelected()`/`getIsAllPageRowsSelected()` exclude those descendants from the all-selected computation, so the header checkbox still reads checked. Deselect-all skips rows whose `enableRowSelection` resolves false, preserving their selection; use `toggleAllRowsSelected(false, { deselectAll: true })` or `resetRowSelection(true)` to clear everything including disabled and out-of-model ids. ## Common Mistakes ### [HIGH] Expecting selection to clean itself Wrong: `data = data.filter(row => row.id !== deletedId)` Correct: `data = data.filter(row => row.id !== deletedId); table.setRowSelection(old => { const next = { ...old }; delete next[deletedId]; return next })` Selection is independent state and can retain IDs after data removal. Source: `https://github.com/TanStack/table/issues/5850` ### [HIGH] Selecting mutable indexes Wrong: `const options = { getRowId: (_row: Person, index: number) => String(index) }` Correct: `const options = { getRowId: (row: Person) => row.id }` Stable application IDs preserve identity as row order and pages change. Source: `docs/framework/react/guide/row-selection.md#useful-row-ids` ### [HIGH] Treating loaded model as global selection Wrong: `const allSelectedRecords = table.getSelectedRowModel().rows` Correct: `const allSelectedIds = table.getSelectedRowIds()` Under manual pagination, unloaded selected IDs have no `Row` object in the current model. Source: `docs/framework/react/guide/row-selection.md#note-if-you-are-using-manualpagination` ### [HIGH] Bypassing the range-selection handler Wrong: ```ts const onChange = (event: { target: { checked: boolean } }) => row.toggleSelected(event.target.checked) ``` Correct: ```ts const onChange = row.getToggleSelectedHandler() ``` Only successful interactions through `getToggleSelectedHandler()` establish or advance the Shift-range anchor. The handler also supports custom range-event detection through `isRowRangeSelectionEvent`. Source: `docs/framework/react/guide/row-selection.md#shift-range-selection` ### [HIGH] Binding a DOM change event that drops Shift Wrong: ```ts checkbox.addEventListener('change', row.getToggleSelectedHandler()) ``` Correct: ```ts checkbox.addEventListener('click', row.getToggleSelectedHandler()) ``` The range modifier must be present on the event passed to the handler. Raw DOM `change` events do not reliably expose click modifier keys. Source: `docs/framework/svelte/guide/row-selection.md#shift-range-selection` ### [MEDIUM] Expecting ranges across unloaded server pages Wrong: ```ts const options = { manualPagination: true, enableRowRangeSelection: true, } // Shift cannot select rows absent from data. ``` Correct: ```ts const options = { manualPagination: true, getRowId: (row: Person) => row.id, } ``` Client-side ranges can cross pages because display order is pre-pagination. Manual/server pagination cannot include rows absent from the loaded `data`; select database-wide IDs in application state when that behavior is required. Source: `docs/framework/react/guide/row-selection.md#shift-range-selection`, `https://github.com/TanStack/table/issues/4781` ## API Discovery Inspect `node_modules/@tanstack/table-core/dist/features/row-selection/` for state, row-model variants, and selection enablement callbacks.