Angular does not ship a legacy v8 API in v9 (unlike React's
useLegacyTable). You migrate directly to v9's injectTable + _features +
_rowModels shape. There is no incremental in-place adapter — the public
entrypoint name itself changes.
This skill is a mechanical translation table. Work through it top-to-bottom.
For exhaustive lookup tables (row-model mapping, feature registration, type
generics, sorting renames, sizing-vs-resizing split, etc.) →
references/v8-to-v9-mapping.md.
Key behavioral change: the injectTable initializer re-runs when signals
inside it change, then the adapter calls table.setOptions({ ...prev, ...new }).
Move stable values (columns, _features, _rowModels) outside the
initializer so they aren't recreated on every data update.
2. Required new options: _features + _rowModels
v9 is opt-in for every feature. Both options are required.
In Angular, all three (table.atoms.<slice>, table.store.state,
table.baseAtoms.<slice>) are signal-backed — reading them inside a template,
computed(...), or effect(...) registers an Angular dependency
automatically. No toSignal(...) wrappers needed.
See tanstack-table/angular/table-state for the full state surface mental
model.
4. Controlled state — on[State]Change shape
The shape is largely the same. onStateChange (the single global v8 hook) is
gone in v9. Slices are controlled individually via state.<slice> +
on[State]Change callbacks. Each callback receives either a new value or an
updater function:
Always check updater instanceof Function (or typeof updater === 'function').
TanStack Table calls the callback with both shapes depending on the
transition.
If you don't want to repeat TFeatures everywhere, use createTableHook(...)
and the resulting createAppColumnHelper<Person>() which pre-binds features.
Every public type now requires TFeatures (ColumnDef<TFeatures, TData, TValue>,
Cell<TFeatures, TData, TValue>, etc.). Full mapping →
references/v8-to-v9-mapping.md.
6. Rendering — directive-based, with shorthand directives
v8 Angular rendering was already directive-flavored, but v9 adds the
shorthand directives (*flexRenderCell, *flexRenderHeader,
*flexRenderFooter) that auto-resolve the column-def slot and context. Prefer
them over the long *flexRender="… ; props: …" form.
<!-- v8 / v9 long form (still works) --><td
*flexRender="cell.column.columnDef.cell; props: cell.getContext(); let rendered"
>
{{ rendered }}
</td><!-- v9 shorthand — recommended --><td *flexRenderCell="cell; let value">{{ value }}</td><th *flexRenderHeader="header; let value">{{ value }}</th><th *flexRenderFooter="footer; let value">{{ value }}</th>
DI tokens (TanStackTable / TanStackTableHeader / TanStackTableCell
directives + injectTableContext() / injectTableHeaderContext() /
injectTableCellContext() / injectFlexRenderContext()) — no more input
drilling.
Column-def cell / header / footer functions run inside
runInInjectionContext, so inject(...) and signals work in them.
See tanstack-table/angular/angular-rendering-directives for the full surface.
7. Reactivity model — signals replace v8 memo accessors
v8 backed reactivity with manual memoized getters. v9's adapter
(angularReactivity(injector)) backs every readonly atom with an Angular
computed and every writable atom with an Angular signal. Consequences:
No toSignal(...) adapters around table state. Read table.atoms.x.get()
/ table.store.state.x directly inside templates, computed, effect.
computed(...) is for derivation / equality, not for "make it reactive".
Use { equal: shallow } from @tanstack/angular-table on object/array
slices to skip downstream work on no-op updates.
The injectTable initializer re-runs on signal changes. Don't put
expensive object literals in there.
Replace columnSizingInfo state / setters / change handler with the
columnResizing equivalents; add columnResizingFeature to _features
if you actually drag-resize.
Replace enablePinning with enableColumnPinning / enableRowPinning.
Update ColumnMeta module augmentation to include the TFeatures
generic.
Drop any _-prefixed internal API usages; replace with public
equivalents.
(Optional) Adopt tableOptions(...) for shared base config.
(Optional) Adopt createTableHook(...) for app-wide table infrastructure.
Failure modes
1. (CRITICAL) Leaving getCoreRowModel() / getSortedRowModel() / etc. in v9 options
These options don't exist anymore. They become _rowModels entries with
factory functions. The TypeScript error is loud but agents sometimes silence
it with as any — don't.
2. (CRITICAL) Reaching for createAngularTable from v8 muscle memory
Always injectTable(() => ({...})). The injection-context requirement means
it must run from a class field, constructor, or
runInInjectionContext(injector, () => injectTable(...)).
3. (CRITICAL) Registering a feature but forgetting its row model
// ❌ filtering enabled, but no filtered row model — UI changes, rows don't filter_features: tableFeatures({ columnFilteringFeature })
_rowModels: {
} // missing filteredRowModel// ✅_rowModels: {
filteredRowModel: createFilteredRowModel(filterFns)
}
Same for sorting, pagination, expanding, grouping, faceting. Selection,
visibility, ordering, pinning, sizing, resizing do not need a row model.
4. (HIGH) getState() → table.store.state text replacement loses reactivity
Bulk-replacing table.getState().x with table.store.state.x works for current
value reads, but if you used a computed/memo around getState() for
reactivity, switch to table.atoms.x.get() — it's already signal-backed and
needs no wrapper.
5. (HIGH) Stale sortingFn / sortingFns references in column defs
The rename is mechanical: sortingFn → sortFn, sortingFns → sortFns,
getSortingFn → getSortFn. Missed renames produce silent runtime fallbacks
to default sort.
6. (HIGH) Column helper using v8 generic order
// ❌ TS will complain — the first generic is TFeatures, not TDataconst columnHelper = createColumnHelper<Person>()
The v8 mental model was "build columns inside the hook". v9's
injectTable initializer re-runs on every signal read change — keep heavy
literals outside.