Capítulo 24 de 54

Chapter 24: Composable Tables (createTableHook) Guide

Core Idea

createTableHook({ features, ...defaults }) builds an app-specific useAppTable + createAppColumnHelper pair so every table in an app shares one feature set, row models, and default options without re-declaring tableFeatures() per table — and optionally registers reusable cell/header/table components too.

Key Concepts

  • Two-tier setup: define features/shared defaults (debugTable, enableSortingRemoval, etc.) once via createTableHook; each table's call site (useAppTable({ key, columns, data, ...overrides }, selector)) only supplies what's unique to it. Per-call options override the hook's defaults for that one table.
  • createAppColumnHelper<TData>() is already bound to the app's features, so individual tables don't thread typeof features through every column definition — a real ergonomic win once you have more than one or two tables.
  • Rendering an app-hook table works with the plain table APIs (table.getHeaderGroups(), table.FlexRender) exactly like a standalone useTable table — you don't need the component-registry machinery to benefit from shared features/options.
  • Optional component registry: createTableHook can also accept tableComponents/cellComponents/headerComponents maps. Column definitions then reference them directly inside cell/header (e.g. cell: ({ cell }) => <cell.TextCell />), and registered components read the current instance via useTableContext()/useCellContext()/useHeaderContext() (React context, not props) instead of needing that instance threaded down manually.
  • Once components are registered, table.AppTable, table.AppCell, table.AppHeader, table.AppFooter are the wrappers that provide those context values while rendering — <table.AppTable selector={...}> also doubles as the state-subscription boundary for that render, same pattern as table.Subscribe.
  • Import the context hooks from the same module that called createTableHook — that's what makes table.PaginationControls, cell.TextCell, header.SortIndicator fully typed (bound to your TFeatures and your registered component maps).
  • Two or more row types (e.g. Person and Product) can share one createTableHook call — each gets its own createAppColumnHelper<TData>() bound to the same shared features/components, while each table still owns its own data and columns.
  • Scoped contexts: by default all useAppTable tables share one module-scoped context, which is fine for independent tables anywhere in the tree. Only reach for createTableHookContexts (a separate isolated context set passed into createTableHook) when you nest one table setup's provider inside another's and a consumer would otherwise read the wrong (nearest) provider.

Code Examples

// hooks/table.ts — shared setup, once
const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns })
export const { useAppTable, createAppColumnHelper } = createTableHook({ features, debugTable: true })

// PersonTable.tsx — per-table
const columnHelper = createAppColumnHelper<Person>()
const columns = columnHelper.columns([columnHelper.accessor('firstName', { cell: (i) => i.getValue() })])

function PersonTable({ data }: { data: Person[] }) {
  const table = useAppTable({ key: 'people', columns, data }, (s) => ({ sorting: s.sorting }))
  return <table>{/* table.getHeaderGroups() / table.getRowModel(), same as a plain useTable table */}</table>
}
  • What it demonstrates: createAppColumnHelper and useAppTable behave exactly like createColumnHelper/useTable, just pre-bound to shared config — no new mental model, just less repetition.

Key Takeaways

  1. Reach for createTableHook as soon as an app has more than one table sharing the same features/defaults — it removes the repeated tableFeatures() boilerplate and keeps every table's typeof features consistent.
  2. The component-registry half is optional and separate from the shared-config half — adopt it only when several tables should share literal UI pieces (toolbars, cell renderers), not just config.
  3. Default (shared, module-scoped) context is correct for the vast majority of apps; only reach for createTableHookContexts when table setups are genuinely nested.

Connects To

  • Table Context Guide: the full mechanics of how registered components read table/cell/header from context, including the "why table itself is not a stable context value" gotcha.
  • Custom Features (React) Guide: writing the app-specific features that get shared via this same tableFeatures()/createTableHook pattern.
  • Helpers Guide: createColumnHelper, the non-app-hook version createAppColumnHelper builds on.