| title | Migrating to TanStack Table v9 (Angular) |
|---|
TanStack Table v9 is a major release that introduces significant architectural improvements while maintaining the core table logic you're familiar with. Here are the key changes:
- Features are tree-shakeable: Features are now treated as plugins: import only what you use. If your table only needs sorting, you won't ship filtering, pagination, or other feature code. Bundlers can eliminate unused code, so for smaller tables you can expect a meaningfully smaller bundle compared to v8. This also lets TanStack Table add features over time without bloating everyone's bundles.
- Row models and their functions are refactored: Row model factories (
createFilteredRowModel,createSortedRowModel, etc.) now accept their processing functions (filterFns,sortFns,aggregationFns) as parameters. This enables tree-shaking of the functions themselves: if you use a custom filter, you don't pay for built-in filters you never use.
- Uses TanStack Store: The internal state system has been rebuilt on TanStack Store, providing a reactive, framework-agnostic foundation.
- Opt-in subscriptions instead of memo hacks: In Angular, table atoms are backed by signals. Use
computed(...)when you want selector-style derivation or custom equality, and keep reads scoped to the state you actually need.
tableOptions: New utilities let you compose and share table configurations. Definefeatures,rowModels, and default options once, then reuse them across tables or pass them throughcreateTableHook.createTableHook(optional, advanced): Create reusable, strongly typed Angular table factories with pre-bound features, row models, default options, and component registries.
While v9 is a significant upgrade, you don't have to adopt everything at once:
- Don't want to think about tree-shaking yet? You can start with
stockFeaturesto include most commonly used features. - Your table markup is largely unchanged. How you render
<table>,<thead>,<tr>,<td>, etc. remains the same.
The main change is how you define a table with the Angular adapter, specifically the new features and rowModels options.
The Angular adapter entrypoint to create a table instance is injectTable:
// v8
import { createAngularTable } from '@tanstack/angular-table'
const v8Table = createAngularTable(() => ({
// options
}))
// v9
import { injectTable } from '@tanstack/angular-table'
const v9Table = injectTable(() => ({
// options
}))Note:
injectTableevaluates your initializer whenever any Angular signal read inside of it changes. Keep expensive/static values (likecolumns,features, androwModels) as stable references outside the initializer.
In v9, you must explicitly declare which features and row models your table uses:
// v8
import { createAngularTable, getCoreRowModel } from '@tanstack/angular-table'
const v8Table = createAngularTable(() => ({
columns,
data: data(),
getCoreRowModel: getCoreRowModel(),
}))
// v9
import {
injectTable,
tableFeatures,
} from '@tanstack/angular-table'
const features = tableFeatures({}) // Empty = core features only
// Define stable references outside the initializer
const v9Table = injectTable(() => ({
features,
rowModels: {}, // Core row model is automatic
columns: this.columns,
data: this.data(),
}))Features control what table functionality is available. In v8, all features were bundled. In v9, you import only what you need.
import {
tableFeatures,
// Import only the features you need
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
columnVisibilityFeature,
rowSelectionFeature,
} from '@tanstack/angular-table'
// Create a features object (define this outside your injectTable initializer for stable reference)
const features = tableFeatures({
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
columnVisibilityFeature,
rowSelectionFeature,
})If you want all features without thinking about it (like v8), import stockFeatures:
import { injectTable, stockFeatures } from '@tanstack/angular-table'
class TableCmp {
readonly table = injectTable(() => ({
features: stockFeatures, // All features included
rowModels: { /* ... */ },
columns: this.columns,
data: this.data(),
}))
}| Feature | Import Name |
|---|---|
| Column Filtering | columnFilteringFeature |
| Global Filtering | globalFilteringFeature |
| Row Sorting | rowSortingFeature |
| Row Pagination | rowPaginationFeature |
| Row Selection | rowSelectionFeature |
| Row Expanding | rowExpandingFeature |
| Row Pinning | rowPinningFeature |
| Column Pinning | columnPinningFeature |
| Column Visibility | columnVisibilityFeature |
| Column Ordering | columnOrderingFeature |
| Column Sizing | columnSizingFeature |
| Column Resizing | columnResizingFeature |
| Column Grouping | columnGroupingFeature |
| Column Faceting | columnFacetingFeature |
Row models are the functions that process your data (filtering, sorting, pagination, etc.). In v9, they're configured via rowModels instead of get*RowModel options.
| v8 Option | v9 rowModels Key |
v9 Factory Function |
|---|---|---|
getCoreRowModel() |
(automatic) | Not needed, always included |
getFilteredRowModel() |
filteredRowModel |
createFilteredRowModel(filterFns) |
getSortedRowModel() |
sortedRowModel |
createSortedRowModel(sortFns) |
getPaginationRowModel() |
paginatedRowModel |
createPaginatedRowModel() |
getExpandedRowModel() |
expandedRowModel |
createExpandedRowModel() |
getGroupedRowModel() |
groupedRowModel |
createGroupedRowModel(aggregationFns) |
getFacetedRowModel() |
facetedRowModel |
createFacetedRowModel() |
getFacetedMinMaxValues() |
facetedMinMaxValues |
createFacetedMinMaxValues() |
getFacetedUniqueValues() |
facetedUniqueValues |
createFacetedUniqueValues() |
Several row model factories now accept their processing functions as parameters. This enables better tree-shaking and explicit configuration:
import {
injectTable,
createFilteredRowModel,
createSortedRowModel,
createGroupedRowModel,
createPaginatedRowModel,
filterFns, // Built-in filter functions
sortFns, // Built-in sort functions
aggregationFns, // Built-in aggregation functions
} from '@tanstack/angular-table'
class TableCmp {
readonly table = injectTable(() => ({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
sortedRowModel: createSortedRowModel(sortFns),
groupedRowModel: createGroupedRowModel(aggregationFns),
paginatedRowModel: createPaginatedRowModel(),
},
columns: this.columns,
data: this.data(),
}))
}// v8
import {
injectTable,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
getPaginationRowModel,
filterFns,
sortingFns,
} from '@tanstack/angular-table'
const v8Table = createAngularTable(() => ({
columns,
data: data(),
getCoreRowModel: getCoreRowModel(), // used to be called "get*RowModel()"
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
filterFns, // used to be passed in as a root option
sortingFns,
}))
// v9
import {
injectTable,
tableFeatures,
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
createFilteredRowModel,
createSortedRowModel,
createPaginatedRowModel,
filterFns,
sortFns,
} from '@tanstack/angular-table'
const features = tableFeatures({
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
})
const v9Table = injectTable(() => ({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
sortedRowModel: createSortedRowModel(sortFns),
paginatedRowModel: createPaginatedRowModel(),
},
columns,
data: data(),
}))In v8, you accessed state via table.getState(). In v9, read the specific
state slice from table.atoms.<slice>.get() where possible. Use table.store.get()
when you need the full flat state shape, such as debug JSON.
// v8
const state = table.getState()
const v8 = table.getState()
const { sorting, pagination } = v8
// v9 - per-slice reads, preferred for Angular render code
const sorting = table.atoms.sorting.get()
const pagination = table.atoms.pagination.get()
// v9 - full-state flat snapshot
const fullState = table.store.get()
const v9 = table.store.get()
const { sorting: v9Sorting, pagination: v9Pagination } = v9In Angular, you have a few good options for consuming table state.
The Angular adapter backs table atoms with Angular signals. Read the atom you care about directly in templates, effects, or computed values.
import { computed, effect } from '@angular/core'
import { shallow } from '@tanstack/angular-table'
class TableCmp {
readonly table = injectTable(() => ({
features,
rowModels: { /* ... */ },
columns: this.columns,
data: this.data(),
}))
// Use computed when deriving from a slice or applying equality.
private readonly pagination = computed(
() => this.table.atoms.pagination.get(),
{ equal: shallow },
)
constructor() {
effect(() => {
const { pageIndex, pageSize } = this.pagination()
console.log('Page', pageIndex, 'Size', pageSize)
})
}
}Use Angular computed(...) when you want selector-style behavior, a derived value, or an equality function. For object/array slices, use shallow from @tanstack/angular-table to avoid unnecessary downstream work when the slice is recreated with the same values.
import { computed, effect } from '@angular/core'
import { shallow } from '@tanstack/angular-table'
class TableCmp {
readonly table = injectTable(() => ({
features,
rowModels: { /* ... */ },
columns: this.columns,
data: this.data(),
}))
// Provide an equality function for object slices
readonly pagination = computed(
() => this.table.atoms.pagination.get(),
{ equal: shallow },
)
constructor() {
effect(() => {
// This effect only re-runs when pagination changes
const { pageIndex, pageSize } = this.pagination()
console.log('Page', pageIndex, 'Size', pageSize)
})
}
}The v8-style state + on[State]Change controlled state patterns still work and remain convenient for simple integrations. For new v9 code, prefer owning state slices with external atoms via the new atoms table option (created with createAtom from @tanstack/angular-store), which give you fine-grained subscriptions without mirroring state through Angular signals. See the External Atoms section of the Table State Guide and the Basic External Atoms example.
import { signal } from '@angular/core'
import type { SortingState, PaginationState } from '@tanstack/angular-table'
class TableCmp {
readonly sorting = signal<SortingState>([])
readonly pagination = signal<PaginationState>({ pageIndex: 0, pageSize: 10 })
readonly table = injectTable(() => ({
features,
rowModels: { /* ... */ },
columns: this.columns,
data: this.data(),
state: {
sorting: this.sorting(),
pagination: this.pagination(),
},
onSortingChange: (updater) => {
updater instanceof Function
? this.sorting.update(updater)
: this.sorting.set(updater)
},
onPaginationChange: (updater) => {
updater instanceof Function
? this.pagination.update(updater)
: this.pagination.set(updater)
},
}))
}The createColumnHelper function now requires a TFeatures type parameter in addition to TData:
// v8
import { createColumnHelper } from '@tanstack/angular-table'
const columnHelperV8 = createColumnHelper<Person>()
// v9
import { createColumnHelper, tableFeatures, rowSortingFeature } from '@tanstack/angular-table'
const features = tableFeatures({ rowSortingFeature })
const columnHelperV9 = createColumnHelper<typeof features, Person>()v9 adds a columns() helper for better type inference when wrapping column arrays.
const columnHelper = createColumnHelper<typeof features, Person>()
// Wrap your columns array for better type inference
const columns = columnHelper.columns([
columnHelper.accessor('firstName', {
header: 'First Name',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('lastName', {
id: 'lastName',
header: () => 'Last Name',
cell: (info) => info.getValue(),
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: () => 'Edit',
}),
])When using createTableHook, you get a pre-bound createAppColumnHelper that only requires TData:
import { createTableHook, tableFeatures, rowSortingFeature } from '@tanstack/angular-table'
const features = tableFeatures({ rowSortingFeature })
const { injectAppTable, createAppColumnHelper } = createTableHook({
features,
rowModels: { /* ... */ },
})
// TFeatures is already bound, only need TData!
const columnHelper = createAppColumnHelper<Person>()The rendering primitives in the Angular adapter are FlexRender and the *flexRender directives.
In v9, you can continue to render header/cell/footer content using the Angular adapter rendering utilities, but there are a few important improvements and helper APIs to be aware of.
Angular rendering is directive-based:
FlexRender/*flexRenderrenders arbitrary render content (primitives,TemplateRef, component types, orflexRenderComponent(...)wrappers)- The directive is responsible for mounting embedded views or components via
ViewContainerRef
If you're rendering standard table content, prefer the shorthand helpers:
*flexRenderCell="cell; let value"*flexRenderHeader="header; let value"*flexRenderFooter="footer; let value"
These automatically select the correct column definition (columnDef.cell / header / footer) and the right props (cell.getContext() / header.getContext()), so you don't need to manually provide props:.
Column definition render functions (header, cell, footer) run inside an Angular injection context, so they can safely call inject() and use signals.
When a component is rendered through the FlexRender directives, you can also access the full render props object via DI using injectFlexRenderContext().
If you need to render an Angular component with explicit configuration (custom inputs, outputs, injector, and Angular v20+ creation-time bindings/directives), return a flexRenderComponent(Component, options) wrapper from your column definition.
For complete rendering details (including component rendering, TemplateRef, flexRenderComponent, and context helpers), see the Rendering components Guide.
The tableOptions() helper provides type-safe composition of table options. It's useful for creating reusable partial configurations that can be spread into your table setup.
import { injectTable, tableOptions, tableFeatures, rowSortingFeature } from '@tanstack/angular-table'
import { isDevMode } from '@angular/core';
const features = tableFeatures({ rowSortingFeature })
// Create a reusable options object with features pre-configured
const baseOptions = tableOptions({
features,
debugTable: isDevMode()
})
class TableCmp {
readonly table = injectTable(() => ({
...baseOptions,
columns: this.columns,
data: this.data(),
rowModels: {},
}))
}tableOptions() allows you to omit certain required fields (like data, columns, or features) when creating partial configurations:
import {
tableOptions,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
createSortedRowModel,
createFilteredRowModel,
filterFns,
sortFns,
} from '@tanstack/angular-table'
const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
})
// Partial options without data or columns
const featureOptions = tableOptions({
features,
rowModels: {
sortedRowModel: createSortedRowModel(sortFns),
filteredRowModel: createFilteredRowModel(filterFns),
},
})import { injectTable, tableOptions, createPaginatedRowModel } from '@tanstack/angular-table'
// Another partial without features (inherits from spread)
const paginationDefaults = tableOptions({
rowModels: {
paginatedRowModel: createPaginatedRowModel(),
},
initialState: {
pagination: { pageIndex: 0, pageSize: 25 },
},
})
class TableCmp {
readonly table = injectTable(() => ({
...featureOptions,
...paginationDefaults,
columns: this.columns,
data: this.data(),
}))
}tableOptions() pairs well with createTableHook for building composable table factories:
import {
createTableHook,
tableOptions,
tableFeatures,
rowSortingFeature,
rowPaginationFeature,
createSortedRowModel,
createPaginatedRowModel,
sortFns,
} from '@tanstack/angular-table'
const features = tableFeatures({ rowSortingFeature, rowPaginationFeature })
const sharedOptions = tableOptions({
features,
rowModels: {
sortedRowModel: createSortedRowModel(sortFns),
paginatedRowModel: createPaginatedRowModel(),
},
})
const { injectAppTable } = createTableHook(sharedOptions)This is an advanced, optional feature. You don't need to use createTableHook; injectTable is sufficient for most use cases.
For applications with multiple tables sharing the same configuration, createTableHook lets you define features, row models, and reusable components once.
For full setup and patterns, see the Composable Tables Guide.
The enablePinning option has been split into separate options:
// v8
enablePinning: true
// v9
enableColumnPinning: true
enableRowPinning: trueAll internal APIs prefixed with _ have been removed. If you were using any of these, use their public equivalents.
In v8, column sizing and resizing were combined in a single feature. In v9, they've been split into separate features for better tree-shaking.
| v8 | v9 |
|---|---|
ColumnSizing (combined feature) |
columnSizingFeature + columnResizingFeature |
columnSizingInfo state |
columnResizing state |
setColumnSizingInfo() |
setcolumnResizing() (note the lowercase c, the current v9 spelling) |
onColumnSizingInfoChange option |
onColumnResizingChange option |
If you only need column sizing (fixed widths) without interactive resizing, you can import just columnSizingFeature. If you need drag-to-resize functionality, import both.
Sorting-related APIs have been renamed for consistency:
| v8 | v9 |
|---|---|
sortingFn (column def option) |
sortFn |
column.getSortingFn() |
column.getSortFn() |
column.getAutoSortingFn() |
column.getAutoSortFn() |
SortingFn type |
SortFn type |
SortingFns interface |
SortFns interface |
sortingFns (built-in functions) |
sortFns |
Update your column definitions.
Some row APIs have changed from private to public:
| v8 | v9 |
|---|---|
row._getAllCellsByColumnId() (private) |
row.getAllCellsByColumnId() (public) |
Most types now require a TFeatures parameter:
// v8
type Column<TData>
type ColumnDef<TData>
type Table<TData>
type Row<TData>
type Cell<TData, TValue>
// v9
type Column<TFeatures, TData, TValue>
type ColumnDef<TFeatures, TData, TValue>
type Table<TFeatures, TData>
type Row<TFeatures, TData>
type Cell<TFeatures, TData, TValue>The easiest way to get the TFeatures type is with typeof:
const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
})
type MyFeatures = typeof features
const columns: ColumnDef<typeof features, Person>[] = [...]If using stockFeatures, use the StockFeatures type:
import type { StockFeatures, ColumnDef } from '@tanstack/angular-table'
const columns: ColumnDef<StockFeatures, Person>[] = [...]No more declaration merging required! (Although it still works if you want to keep using it)
Global declaration merging to extend TableMeta or ColumnMeta works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take TFeatures as the first type parameter.
Optionally, v9 also adds a new way to declare meta types per-table without declaration merging. You can use type-only tableMeta/columnMeta slots on the features option, which only affect tables created with that features object:
const features = tableFeatures({
rowSortingFeature,
columnMeta: metaHelper<{ customProperty: string }>(),
})See the new Table and Column Meta Guide for full details on both approaches.
The RowData type is now more restrictive:
// v8 - very permissive
type RowData = unknown
// v9 - must be a record or array
type RowData = Record<string, any> | Array<any>This change improves type safety. If you were passing unusual data types, ensure your data conforms to Record<string, any> or Array<any>.
- Update your table setup to v9 and define
featuresusingtableFeatures()(or usestockFeatures) - Migrate
get*RowModel()options torowModels - Update row model factories to include
Fnsparameters where needed - Update TypeScript types to include
TFeaturesgeneric - Update state access:
table.getState().slice→table.atoms.<slice>.get()where possible; usetable.store.get()for full-state/debug reads - Update
createColumnHelper<TData>()→createColumnHelper<TFeatures, TData>() - Replace
enablePinningwithenableColumnPinning/enableRowPinningif used - Rename
sortingFn→sortFnin column definitions - Split column sizing/resizing: use both
columnSizingFeatureandcolumnResizingFeatureif needed - Rename
columnSizingInfostate →columnResizing(and related options) - If you use
TableMeta/ColumnMetadeclaration merging, add theTFeaturesgeneric to your augmentations (optionally, switch to the per-tabletableMeta/columnMetafeature slots) - (Optional) Use
tableOptions()for composable configurations - (Optional) Use
createTableHookfor reusable table patterns
Check out these examples to see v9 patterns in action: