@tanstack/table-core
Version:
Headless UI for building powerful tables & datagrids for TS/JS.
236 lines (208 loc) • 9.75 kB
Markdown
---
name: custom-features
description: >
Author a TanStack Table v9 feature plugin across every FeatureMap and API installation surface: state, options, column definitions, table, column, row, cell, header, row-model functions/caches, defaults, prototypes, and table/row/column instance data lifecycles. Load for initTableInstanceData, resetTableInstanceData, constructTableAPIs, or reusable behavior not covered by built-ins, meta, or option composition.
metadata:
type: sub-skill
library: '/table-core'
library_version: '9.0.0'
requires: ['core', 'table-features', 'typescript']
sources:
- 'TanStack/table:docs/framework/react/guide/custom-features.md'
- 'TanStack/table:packages/table-core/src/types'
- 'TanStack/table:packages/table-core/src/types/TableFeatures.ts'
- 'TanStack/table:packages/table-core/src/utils.ts'
- 'TanStack/table:packages/table-core/src/features'
- 'TanStack/table:examples/react/custom-plugin'
---
This skill builds on `core`, `table-features`, and `typescript`. Prefer a built-in feature or typed `tableMeta`/`columnMeta` for application callbacks; create a feature only for reusable state or behavior that must augment Table objects.
## Extension Surface
Declaration-merge `Plugins` to register the feature key, then merge only the maps backed by runtime behavior:
| Feature map | Contribution |
| ------------------------------------------------ | ----------------------------------------- |
| `TableState_FeatureMap` | State slices in options, atoms, and store |
| `TableOptions_FeatureMap<TFeatures, TData>` | Table options and change callbacks |
| `Table_FeatureMap<TFeatures, TData>` | Public table APIs |
| `ColumnDef_FeatureMap<TFeatures, TData, TValue>` | Column-definition options |
| `Column_FeatureMap<TFeatures, TData>` | Public column APIs/data |
| `Row_FeatureMap<TFeatures, TData>` | Public row APIs/data |
| `Cell_FeatureMap` | Public cell APIs |
| `Header_FeatureMap` | Public header APIs |
| `RowModelFns_FeatureMap<TFeatures, TData>` | Advanced `table._rowModelFns` registries |
| `CachedRowModels_FeatureMap<TFeatures, TData>` | Advanced `table._rowModels` cache getters |
`assignTableAPIs` installs singleton table methods. Use `assignPrototypeAPIs` inside `assignColumnPrototype`, `assignRowPrototype`, `assignCellPrototype`, or `assignHeaderPrototype` for shared object methods. Use `initColumnInstanceData` and `initRowInstanceData` only for per-instance fields.
Use `initTableInstanceData` for mutable, non-reactive data owned by one table. It runs once after options, state atoms, and the store exist, and every feature initialization finishes before any `constructTableAPIs` hook runs. Use `resetTableInstanceData` to clear transient instance data after internal atoms reset during `table.reset()`; it does not own state slices or externally controlled state. Keep `constructTableAPIs` exclusively for method assignment.
## Table Instance Lifecycle
```ts
import type { TableFeature } from '@tanstack/table-core'
interface InteractionData {
_interactionHistory: Set<string>
}
const interactionData = (table: object): InteractionData =>
table as unknown as InteractionData
export const interactionFeature: TableFeature = {
initTableInstanceData: (table) => {
interactionData(table)._interactionHistory = new Set()
},
resetTableInstanceData: (table) => {
interactionData(table)._interactionHistory.clear()
},
}
```
Initialization may allocate resources once while reset clears their transient contents. Do not rerun initialization during `table.reset()`.
## Complete Example
This single example shows every ordinary FeatureMap and API installation path. It names the two advanced row-model maps without enabling them because density does not own the row-model pipeline.
<!-- skill-snippet:check -->
```ts
import {
assignPrototypeAPIs,
assignTableAPIs,
functionalUpdate,
makeStateUpdater,
tableFeatures,
type CellData,
type OnChangeFn,
type RowData,
type TableFeature,
type TableFeatures,
type Updater,
} from '/table-core'
type Density = 'sm' | 'md'
interface DensityState {
density: Density
}
interface DensityOptions {
onDensityChange?: OnChangeFn<Density>
}
interface DensityColumnDef {
enableDensity?: boolean
}
interface DensityAPI {
getDensity: () => Density
}
interface DensityTable extends DensityAPI {
setDensity: (updater: Updater<Density>) => void
}
interface DensityColumn extends DensityAPI {
densityAtCreation?: Density
}
interface DensityRow extends DensityAPI {
densityAtCreation?: Density
}
type DensityCell = DensityAPI
type DensityHeader = DensityAPI
declare module '/table-core' {
interface Plugins {
densityFeature: TableFeature
}
interface TableState_FeatureMap {
densityFeature: DensityState
}
interface TableOptions_FeatureMap<
TFeatures extends TableFeatures,
TData extends RowData,
> {
densityFeature: DensityOptions
}
interface Table_FeatureMap<
TFeatures extends TableFeatures,
TData extends RowData,
> {
densityFeature: DensityTable
}
interface ColumnDef_FeatureMap<
TFeatures extends TableFeatures,
TData extends RowData,
TValue extends CellData,
> {
densityFeature: DensityColumnDef
}
interface Column_FeatureMap<
TFeatures extends TableFeatures,
TData extends RowData,
> {
densityFeature: DensityColumn
}
interface Row_FeatureMap<
TFeatures extends TableFeatures,
TData extends RowData,
> {
densityFeature: DensityRow
}
interface Cell_FeatureMap {
densityFeature: DensityCell
}
interface Header_FeatureMap {
densityFeature: DensityHeader
}
// If this feature actually owned a row-model stage, it would also merge
// RowModelFns_FeatureMap and CachedRowModels_FeatureMap here, then populate
// table._rowModelFns/table._rowModels with matching runtime/cache wiring.
}
const readDensity = (table: unknown): Density =>
(
table as {
atoms: { density: { get: () => Density } }
}
).atoms.density.get()
export const densityFeature: TableFeature = {
getInitialState: (state) => ({ density: 'md', ...state }),
getDefaultTableOptions: (table) => ({
onDensityChange: makeStateUpdater('density', table),
}),
constructTableAPIs: (table) => {
assignTableAPIs('densityFeature', table, {
table_getDensity: { fn: () => readDensity(table) },
table_setDensity: {
fn: (updater: Updater<Density>) =>
(table.options as DensityOptions).onDensityChange?.((old) =>
functionalUpdate(updater, old),
),
},
})
},
assignColumnPrototype: (prototype, table) => {
assignPrototypeAPIs('densityFeature', prototype, table, {
column_getDensity: { fn: (column) => readDensity(column.table) },
})
},
assignRowPrototype: (prototype, table) => {
assignPrototypeAPIs('densityFeature', prototype, table, {
row_getDensity: { fn: (row) => readDensity(row.table) },
})
},
assignCellPrototype: (prototype, table) => {
assignPrototypeAPIs('densityFeature', prototype, table, {
cell_getDensity: { fn: (cell) => readDensity(cell.table) },
})
},
assignHeaderPrototype: (prototype, table) => {
assignPrototypeAPIs('densityFeature', prototype, table, {
header_getDensity: { fn: (header) => readDensity(header.table) },
})
},
initColumnInstanceData: (column) => {
;(column as unknown as DensityColumn).densityAtCreation = readDensity(
column.table,
)
},
initRowInstanceData: (row) => {
;(row as unknown as DensityRow).densityAtCreation = readDensity(row.table)
},
}
export const features = tableFeatures({ densityFeature })
```
`getDefaultColumnDef` is also available for overridable column defaults; add it when the feature owns a real column-definition default. Do not add lifecycle hooks merely to fill the surface.
## Guardrails
- Keep the feature object and `tableFeatures({ densityFeature })` result stable.
- Match every declaration-merged API with runtime installation. Types alone do nothing.
- Preserve incoming state in `getInitialState`; put user state after defaults.
- Method keys use `table_`, `column_`, `row_`, `cell_`, or `header_`; the prefix is removed on installation.
- Table API `fn` receives declared arguments. Prototype API `fn` receives the current object first.
- Initialize table-owned mutable data in `initTableInstanceData`, not `constructTableAPIs`; all feature data is initialized before any table API is assigned.
- Clear transient table-owned data in `resetTableInstanceData`. Reset state slices through atoms/updaters, and do not expect this hook to reset externally controlled state.
- Add `memoDeps` only for a genuinely derived method. Prototype methods are shared and must not close over per-object mutable data.
- There are no `assignColumnAPIs`, `assignRowAPIs`, `assignCellAPIs`, or `assignHeaderAPIs`; use `assignPrototypeAPIs` in the matching hook.
- Do not mutate constructed instances ad hoc or use a feature for renderer-only callbacks that belong in meta.
## API Discovery
Inspect exported `*_FeatureMap` interfaces under `node_modules/@tanstack/table-core/dist/types/`, `TableFeature` in `types/TableFeatures.d.ts`, and `assignTableAPIs`/`assignPrototypeAPIs` in `utils.d.ts`. Copy lifecycle shapes—not domain behavior—from the nearest stock feature under `dist/features/`.