Cheatsheet

Cheatsheet

Decision rules

  • Client-side vs server-side: default client-side; go server-side only when the full dataset is expensive/impossible to load, permissions/business rules must be backend-enforced, or the backend indexes better. Row count alone isn't the deciding factor (client-side row models are stress-tested to 15M rows).
  • Registering a feature vs. its row model: always register the feature for state/APIs. Add the row-model factory only for client-side processing; omit it + set manual*: true for server-side.
  • state/onXChange vs. external atom: external atom (atoms option) whenever the value needs to be read elsewhere (query key, another component) without forcing the table owner to re-render. Classic state+onXChange for simple, contained cases or v8-migration convenience.
  • useTable vs useAppTable: useAppTable (via createTableHook) as soon as 2+ tables share features/defaults; plain useTable for a genuine one-off.
  • FlexRender vs flexRender: FlexRender/table.FlexRender by default (handles aggregated-cell fallback, placeholder suppression); flexRender() only when you already have the resolved def+context.
  • Nested component reading row/cell/column/header state: always wrap the state-dependent read in Subscribe, scoped to the narrowest slice — required, not optional, under stable-reference props.

Thresholds & defaults

ThingDefault
size / minSize / maxSize (column)150 / 20 / Number.MAX_SAFE_INTEGER
columnResizeMode'onEnd'
pageSize10 (typical initialState)
sortDescFirstascending-first for strings, descending-first for numbers
sortUndefined1 (undefined sorts last, ascending)
enableSortingRemovaltrue (cycle includes 'none')
keepPinnedRowstrue (pinned rows stay visible despite filter/pagination)
paginateExpandedRowstrue (sub-rows can span pages)
Virtualization threshold (row count where it starts mattering)thousands+ rows, profile before adopting
Worker row models threshold100,000+ client-side rows, last resort

Feature ↔ row model ↔ manual flag

FeatureRow model factoryManual flag
columnFilteringFeaturecreateFilteredRowModel()manualFiltering
globalFilteringFeature(shares filtered row model)manualFiltering
rowSortingFeaturecreateSortedRowModel()manualSorting
columnGroupingFeaturecreateGroupedRowModel()manualGrouping
rowExpandingFeaturecreateExpandedRowModel()manualExpanding
rowPaginationFeaturecreatePaginatedRowModel()manualPagination
columnFacetingFeaturecreateFacetedRowModel() + *MinMaxValues/*UniqueValuescustom factories, no flag
rowAggregationFeature(none — pure computation)manualAggregation

Tells & smells

  • Column id resolution error → accessor function with neither a string header nor explicit id.
  • Row-number column shows wrong numbers after sort/filter → using row.index instead of row.getDisplayIndex().
  • Checkbox/indicator doesn't update after selection/sort change, but parent re-renders fine → a nested component reading state off a stable row/cell/column/header prop with no Subscribe.
  • Table snaps back to page 1 on every keystroke while editing cells → forgot autoResetPageIndex: false (and autoResetExpanded: false if expandable).
  • Facet counts don't update when a sibling filter changes → facet-rendering component isn't wrapped in table.Subscribe over columnFilters.
  • Server-side table doesn't reset to page 1 after a filter/sort change → the client-side row model's auto-reset hook never fires under manual*: true; reset pageIndex by hand in the change handler.
  • rowSpan={0} rendered as an actual attribute → merges the cell down the whole <tbody>; always skip (return null) instead.
  • filterFn/sortFn: 'myFn' fails to type-check → the function isn't registered in the matching tableFeatures() slot; register it or pass it directly instead of by name.
  • Bundle larger than expected → spreading the full built-in filterFns/sortFns/aggregationFns registry instead of registering individual named functions.