| name | angular/production-readiness |
| description | Ship-ready optimizations for Angular Table v9: register only the `_features` you actually use (tree-shake the bundle); keep `columns` / `_features` / `_rowModels` / feature-fn maps as stable references OUTSIDE the `injectTable` initializer; pass only the `*Fns` your data needs to `createSortedRowModel` / `createFilteredRowModel` / `createGroupedRowModel`; use `ChangeDetectionStrategy.OnPush`; lean on signal-backed atoms (`table.atoms.<slice>.get()`) instead of broad `table.store.state` reads where granularity matters; use `{ equal: shallow }` on object/array `computed` selectors; set `getRowId` for stable identity; track by `id` in every `@for`; defer cell components with `flexRenderComponent` only when you need its options; scope DI tokens via `[tanStackTable*]` directives to kill prop drilling.
|
| type | lifecycle |
| library | tanstack-table |
| framework | angular |
| library_version | 9.0.0-alpha.48 |
| requires | ["angular/table-state","angular/getting-started","angular/angular-rendering-directives"] |
| sources | ["TanStack/table:docs/framework/angular/angular-table.md","TanStack/table:docs/framework/angular/guide/table-state.md","TanStack/table:docs/framework/angular/guide/migrating.md","TanStack/table:packages/angular-table/src/injectTable.ts","TanStack/table:packages/angular-table/src/reactivity.ts","TanStack/table:examples/angular/composable-tables/"] |
Production Readiness (Angular Table v9)
Once your table compiles and renders, this is the cost reduction pass.
Angular's signal-backed adapter makes most of v9's perf "free" if you don't
fight it โ the work is mostly about what you don't do: not recreating
objects, not pulling in features you don't use, not over-wrapping with
selectors.
1. Bundle: register only the features you use
The single biggest v9 win is feature tree-shaking. Every feature you put in
tableFeatures({...}) pulls in its code; everything you leave out is dropped
by the bundler.
const _features = stockFeatures
const _features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
columnFilteringFeature,
})
Use stockFeatures to bootstrap during a v8 โ v9 migration, then come back
and curate. The bundle wins only land once you do.
The same applies to feature-fn registries โ pass only the *Fns your data
needs:
import {
createSortedRowModel,
createFilteredRowModel,
sortFns,
filterFns,
} from '@tanstack/angular-table'
_rowModels: {
sortedRowModel: createSortedRowModel(sortFns),
filteredRowModel: createFilteredRowModel(filterFns),
}
_rowModels: {
sortedRowModel: createSortedRowModel({ basic: sortFns.basic, datetime: sortFns.datetime }),
filteredRowModel: createFilteredRowModel({ includesString: filterFns.includesString }),
}
Same logic for aggregationFns if you use grouping.
2. Stable references โ keep them OUTSIDE the initializer
injectTable(() => ({...})) re-runs the initializer every time a signal read
inside it changes and then calls table.setOptions({ ...prev, ...new }).
Anything you create inside the initializer is recreated on every signal
change.
@Component({...})
export class App {
readonly table = injectTable(() => ({
_features: tableFeatures({ rowSortingFeature }),
_rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
columns: [],
data: this.data(),
}))
}
const _features = tableFeatures({ rowSortingFeature })
const _rowModels = { sortedRowModel: createSortedRowModel(sortFns) }
const columns: Array<ColumnDef<typeof _features, Person>> = []
@Component({...})
export class App {
readonly table = injectTable(() => ({
_features,
_rowModels,
columns,
data: this.data(),
}))
}
Same rule for the controlled-state pattern โ keep state: { pagination: this.pagination() }
inside the initializer, but keep the signal definitions on the class.
For shared infrastructure across multiple tables, createTableHook(...) lets
you define _features / _rowModels / default options once at module scope.
3. ChangeDetectionStrategy.OnPush everywhere
Every component that hosts or renders a TanStack Table should be:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
With signal-backed atoms, OnPush is sufficient โ atom reads in the template
are tracked through computed, so Angular schedules a check when the signal
changes. Default change detection causes redundant work on every event.
All examples/angular/* use OnPush. Match that.
4. Read narrowly โ table.atoms.<slice>.get() over table.store.state
Both surfaces are signal-backed. The difference is which signal gets read.
const pageIndex = computed(() => this.table.store.state.pagination.pageIndex)
const pageIndex = computed(() => this.table.atoms.pagination.get().pageIndex)
For most apps the difference is negligible. For high-frequency atoms or
deeply-derived components, prefer per-atom reads.
5. { equal: shallow } on object/array computed
When you derive an object or array slice, downstream effects and computeds
re-run whenever the reference changes โ even if the structural contents
didn't. Use shallow from @tanstack/angular-table to short-circuit:
import { computed } from '@angular/core'
import { shallow } from '@tanstack/angular-table'
readonly pagination = computed(
() => this.table.atoms.pagination.get(),
{ equal: shallow },
)
readonly visibleColumns = computed(
() => this.table.atoms.columnVisibility.get(),
{ equal: shallow },
)
This is not about reactivity โ atoms are reactive already. This is about
skipping no-op downstream recomputations when the slice rebuilds with the same
values.
Don't reach for it on every read. Reserve it for derived selectors whose
downstream is expensive (effects that hit the server, big template
re-renders).
6. track row.id and getRowId
Always provide a stable identity:
readonly table = injectTable(() => ({
getRowId: (row) => row.id,
}))
Then in every @for:
@for (row of table.getRowModel().rows; track row.id) { ... } @for (cell of
row.getVisibleCells(); track cell.id) { ... } @for (header of
headerGroup.headers; track header.id) { ... }
Without getRowId, IDs default to row index โ re-rendering the entire row list
on a sort flip, refetch, or pagination move because Angular thinks every row
is new.
7. Render-cost rules for cells
Cell render fns run for every visible cell on every re-render. Cheap is the
goal.
- Return a primitive when you can.
cell: (info) => info.getValue() is
fastest.
- Return a component class (not a wrapper) when only inputs need wiring.
The renderer's
KeyValueDiffers skips setInput for unchanged values.
- Reach for
flexRenderComponent(...) only for explicit options โ
custom inputs not derived from context, output callbacks, an injector,
Angular v20+ bindings / directives.
- Don't put expensive
inject() calls in render fns. They run inside
runInInjectionContext every render. Inject at the component level and
close over the value.
- Don't allocate inside render fns when you can avoid it. Closures, new
array literals, etc.
Stable input references
For object inputs (like data arrays you pass into a sub-component), keep the
reference stable across renders. KeyValueDiffers is reference-based for
Angular's default input equality, so a { ...obj } literal on every render
defeats it.
8. Kill prop drilling with DI tokens
Passing cell / header / table through 2+ component layers is both
ergonomic noise and a perf hazard (each input has its own diffing cost).
Replace with the host directives + inject helpers:
<td [tanStackTableCell]="cell">
<ng-container *flexRenderCell="cell; let value">{{ value }}</ng-container>
<app-cell-actions />
</td>
export class CellActionsComponent {
readonly cell = injectTableCellContext()
}
Inside *flexRender* components, the tokens are auto-provided (no host
directive needed) โ see tanstack-table/angular/angular-rendering-directives
ยง7.
9. Large data โ let virtualization do the work
A 10k-row table is fine in v9 in terms of state, but rendering 10k rows is
slow. Don't render what's off-screen. Pair with @tanstack/angular-virtual
โ see tanstack-table/angular/compose-with-tanstack-virtual.
Also consider:
- Server-side pagination if data is huge โ see
tanstack-table/angular/client-to-server.
defaultColumn: { size, minSize, maxSize } to set sane sizing defaults if
you've registered columnSizingFeature.
10. Avoid effect(...) for cross-slice sync โ write directly
effect(() => {
const filter = this.globalFilter()
this.pagination.update((p) => ({ ...p, pageIndex: 0 }))
})
onGlobalFilterChange: (u) => {
typeof u === 'function'
? this.globalFilter.update(u)
: this.globalFilter.set(u)
this.pagination.update((p) => ({ ...p, pageIndex: 0 }))
}
The on*Change handler runs synchronously at the source of truth; an effect
runs after Angular's CD pass, which can lead to double renders.
11. Avoid double-controlling a slice
Don't supply both state.x and atoms.x for the same slice โ atom wins
silently, the Angular signal becomes a write-only sink, and you've doubled
the wiring cost. Pick exactly one source of truth per slice (see
tanstack-table/angular/table-state ยง6โยง7).
12. Build hygiene
bundle-stats / source-map-explorer: after curating _features,
verify your final bundle doesn't include retired features. If you see
rowGroupingFeature in the bundle but never imported it, something is
pulling in stockFeatures indirectly.
debugTable: isDevMode() โ only in dev. Don't leave debugTable: true
in production.
13. Quick wins checklist
Failure modes
1. (CRITICAL) Shipping stockFeatures to production
stockFeatures defeats the v9 bundle wins. The migrating skill explicitly
calls this out โ stockFeatures is a v8 โ v9 bootstrap, not a production
end-state.
2. (CRITICAL) Recreating columns / _features / _rowModels inside the
injectTable initializer
The initializer re-runs on every signal read change. New columns reference
triggers full column-model rebuilds โ for big tables this is visibly slow.
Module-scope it.
3. (CRITICAL) Reimplementing what the table already does
Symptoms:
- Manual sort on the data array inside a
computed/effect, then passing
the sorted array to the table.
- Manual pagination math driving
data: paged() โ paginated by the user, not
the table.
- Hand-rolled global filter
.filter(...) inside effect.
All of these are far slower than the built-in row models (which memoize and
short-circuit) and ship more code. Use table.setSorting(...),
table.setColumnFilters(...), the registered _rowModels factories.
4. (HIGH) OnPush not set
Default change detection runs on every event in the entire app. Even with
signal-backed atoms, you're paying for unnecessary template checks. OnPush
is the table's idiomatic setting.
5. (HIGH) @for without stable track
@for (row of rows) without a track value at all is a build error in
Angular โฅ17 strict mode; track $index defeats DOM reuse on sort/refetch.
Always track row.id (and getRowId on the table).
6. (HIGH) Over-wrapping every read in computed(...)
readonly pagination = computed(() => this.table.atoms.pagination.get())
The atom is already signal-backed. Use computed for derivation, custom
equality, or shared selectors โ not for "make it reactive."
7. (HIGH) { equal: shallow } on every computed
Shallow equality has a runtime cost (one pass over keys). For primitive
selectors it's strictly slower than Object.is. Reserve it for derived
object/array slices whose downstream is expensive.
8. (HIGH) Drilling cell / header / table through multiple
components
Inputs add diffing cost on every change-detection cycle. Replace with the
[tanStackTableCell] / [tanStackTableHeader] / [tanStackTable] host
directives and injectTableCellContext() / etc. at the leaf.
9. (HIGH) flexRenderComponent(...) for every cell
flexRenderComponent adds a wrapper with reflectComponentType overhead
and an OutputEmitterRef subscription scan. For plain component pass-through
where context inputs cover everything, return the component class
directly โ the renderer does setInput on its own.
10. (MEDIUM) effect(...) chains for what should be on*Change inline
If the user changes globalFilter and your pagination reset lives in an
effect, you get a CD pass for the filter and a second one for the reset.
Inline the reset in onGlobalFilterChange.
11. (MEDIUM) Forgetting autoResetPageIndex: false for server-driven tables
Every fetch produces a new array reference, which triggers the default
auto-reset and bounces the user back to page 0 mid-pagination. See
tanstack-table/angular/client-to-server ยง9.
12. (MEDIUM) debugTable: true left in production
Turns on per-operation console.info logging from the core. Use
debugTable: isDevMode().
13. (MEDIUM) Reaching for Subscribe patterns ported from React docs
Angular doesn't need a Subscribe boundary the way React does. The
adapter's signal binding handles fine-grained reactivity at the atom level โ
templates re-evaluate the dependencies they actually read.
See also
tanstack-table/angular/getting-started โ the first-table baseline
tanstack-table/angular/table-state โ narrow vs wide reads, controlled state
tanstack-table/angular/angular-rendering-directives โ flexRenderComponent,
DI tokens
tanstack-table/angular/client-to-server โ server-driven optimizations
tanstack-table/angular/compose-with-tanstack-virtual โ virtualizing big
tables
- Example:
examples/angular/composable-tables/ โ createTableHook for
app-wide infrastructure