Capítulo 11 de 54

Chapter 11: Column Definitions Guide

Core Idea

Column defs are the single most important piece of a table: they build the data model (what's sortable/filterable/groupable), format what's displayed, and can create header/footer groups or pure display-only columns (action buttons, checkboxes) with no underlying data at all.

Key Concepts

  • Three column-def "categories" (not TS types, just a way to talk about them): Accessor columns have a data model (sortable/filterable/groupable); Display columns have no data model (row actions, checkboxes, expanders — can't be sorted/filtered); Grouping columns have no data model either and exist to group other columns under a shared header/footer.
  • createColumnHelper<TFeatures, TData>() (v9 needs both type params — use typeof features for TFeatures) returns .accessor(...), .display(...), .group(...) builders with the best type inference. If you used createTableHook, its returned createAppColumnHelper is already bound to your features type — pass only TData.
  • Three ways to point an accessor column at a value: an object key (accessorKey: 'firstName' or columnHelper.accessor('firstName')), a dot-path into nested objects (accessorKey: 'name.first'), or an accessor function for computed values or array-indexed data (row => row[1], needs an explicit id). Array-indexed accessorKey must be a string ('1', not 1).
  • Column ID resolution: object-key/array-index accessors use that key as the id (periods become underscores); accessor-function columns need either an explicit id or a primitive string header to derive one from — an accessor function with neither is a common source of "column id not found" bugs.
  • Dynamic column definitions (unknown row shape — arbitrary API responses, CSV uploads, user-built reports): type rows as a generic record and build plain ColumnDef objects at runtime from Object.keys(data[0]); the column helper offers no benefit here since there's no static shape to infer against. Two rules: rebuild the array only when data actually changes (memoize it), and since values are unknown, sample a value per key to pick a type-appropriate sortFn/filterFn/filter UI.
  • Rendering hooks: cell (regular cells, via props.getValue(), also has props.row/props.table), aggregatedCell (cells for grouped/aggregated rows — see Aggregation Guide), header, footer. All default to stringifying the raw value if omitted.
  • Feature options live on the column def too: sortFn, filterFn, aggregationFn (string names typed from the registries you passed into tableFeatures, e.g. sortFns/filterFns/aggregationFns — an unregistered name is a type error), plus enable flags like enableSorting/enableColumnFilter.
  • meta attaches arbitrary strongly-typed per-column data (e.g. a filter-variant tag or detected type) — see metaHelper in the Table and Column Meta Guide.

Code Examples

const columnHelper = createColumnHelper<typeof features, Person>()

const columns = columnHelper.columns([
  columnHelper.display({ id: 'actions', cell: (p) => <RowActions row={p.row} /> }),
  columnHelper.group({
    header: 'Name',
    columns: [
      columnHelper.accessor('firstName', { cell: (info) => info.getValue() }),
      columnHelper.accessor((row) => row.lastName, { id: 'lastName', header: 'Last Name' }),
    ],
  }),
  columnHelper.accessor('age', { sortFn: 'basic', filterFn: 'inNumberRange' }),
])
  • What it demonstrates: display, grouping, and two accessor styles (key vs. function) composed via columnHelper.columns([...]), which gives better inference for nested groups than a plain array literal.

Worked Example

For unknown-shape data, skip the column helper entirely and build ColumnDef<typeof features, Record<string, unknown>>[] at runtime — memoized on data so it doesn't rebuild every render:

const columns = useMemo(
  () => data.length
    ? Object.keys(data[0]).map((key) => ({
        accessorKey: key,
        header: formatHeader(key),               // 'firstName' -> 'First Name'
        cell: (info) => String(info.getValue() ?? ''),
      }))
    : [],
  [data],
)

Combine this with per-key sampling (check typeof data[0][key]) to pick a registered sortFn/filterFn name suited to the detected type — a switch on the type is usually enough.

Key Takeaways

  1. Prefer columnHelper over plain object literals whenever the row shape is statically known — the inference difference is significant, especially for nested groups.
  2. Accessor function without a string header or explicit id → column id resolution breaks. Always supply one of the two.
  3. Dynamic columns (unknown shape) are the one case where the column helper adds no value — build ColumnDef[] directly, memoized on data.
  4. Registered function names (sortFn: 'basic') come from the registries wired into tableFeatures; a name TypeScript doesn't recognize means it isn't registered there.

Connects To

  • Table and Column Meta Guide: typing meta with metaHelper.
  • Column & Cell Types (API reference): ColumnDef variants (AccessorColumnDef, DisplayColumnDef, GroupColumnDef).
  • Sorting / Column Filtering / Aggregation (React) Guides: what each registered sortFn/filterFn/aggregationFn name actually does.