CtrlK
BlogDocsLog inGet started
Tessl Logo

row-selection

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.

63

Quality

76%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./packages/table-core/skills/row-selection/SKILL.md
SKILL.md
Quality
Evals
Security

This skill builds on core and table-features. Selection is independent ID state; selected row models can only materialize loaded rows.

Setup

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

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

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

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

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:

const onChange = (event: { target: { checked: boolean } }) =>
  row.toggleSelected(event.target.checked)

Correct:

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:

checkbox.addEventListener('change', row.getToggleSelectedHandler())

Correct:

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:

const options = {
  manualPagination: true,
  enableRowRangeSelection: true,
}
// Shift cannot select rows absent from data.

Correct:

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.

Repository
TanStack/table
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.