TanStack logo

Skill

custom-features

author TanStack Table feature plugins

Covers Architecture Plugin Development TanStack Frontend

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.

SKILL.md

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 mapContribution
TableState_FeatureMapState 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_FeatureMapPublic cell APIs
Header_FeatureMapPublic 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

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.

import {
  assignPrototypeAPIs,
  assignTableAPIs,
  functionalUpdate,
  makeStateUpdater,
  tableFeatures,
  type CellData,
  type OnChangeFn,
  type RowData,
  type TableFeature,
  type TableFeatures,
  type Updater,
} from '@tanstack/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 '@tanstack/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/.

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.