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:
While v9 is a significant upgrade, you don't have to adopt everything at once:
The main change is how you define a table with the Angular adapter — specifically the new _features and _rowModels options.
Angular does not ship a legacy API.
If you're migrating an Angular project from TanStack Table v8 to v9, you will migrate directly to the v9 Angular adapter APIs (injectTable, _features, and _rowModels).
The rest of this guide focuses on migrating to the full v9 API and taking advantage of its features.
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: injectTable evaluates your initializer whenever any Angular signal read inside of it changes. Keep expensive/static values (like columns, _features, and _rowModels) 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 feaFtures 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, state is accessed via the store:
// v8
const state = table.getState()
const v8 = table.getState()
const { sorting, pagination } = v8
// v9 - via the store
const fullState = table.store.state
const v9 = table.store.state
const { sorting: v9Sorting, pagination: v9Pagination } = v9
In Angular, you have a few good options for consuming table state.
TanStack Store lets you subscribe to (and derive) a slice of state. With the Angular adapter, you can use that to create a signal-like value that only updates when the selected slice changes.
This is the closest equivalent to the fine-grained subscription examples you might see in other frameworks.
import { computed, effect } from '@angular/core'
class TableCmp {
readonly table = injectTable(() => ({
_features,
_rowModels: { /* ... */ },
columns: this.columns,
data: this.data(),
}))
// Create a computed to a slice of state.
// The store will only emit when this selected value changes.
private readonly pagination = this.table.computed(
state => state.pagination,
)
constructor() {
effect(() => {
const { pageIndex, pageSize } = this.pagination()
console.log('Page', pageIndex, 'Size', pageSize)
})
}
}
You can also use Angular computed(...) and directly read from table.store.state. This is simple and works well, but for object/array slices you should provide an equality function to avoid unnecessary downstream work when the slice is recreated with the same values.
import { computed, effect } from '@angular/core'
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.store.state.pagination,
{
equal: (a, b) => a.pageIndex === b.pageIndex && a.pageSize === b.pageSize,
},
)
constructor() {
effect(() => {
// This effect only re-runs when pagination changes
const { pageIndex, pageSize } = this.pagination()
console.log('Page', pageIndex, 'Size', pageSize)
})
}
}
Controlled state patterns are pretty the same as v8:
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 { injectAppTable, createAppColumnHelper } = createTableHook({
_features: tableFeatures({ rowSortingFeature }),
_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:
If you're rendering standard table content, prefer the shorthand helpers:
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';
// Create a reusable options object with features pre-configured
const baseOptions = tableOptions({
_features: tableFeatures({ rowSortingFeature }),
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'
// Partial options without data or columns
const featureOptions = tableOptions({
_features: tableFeatures({
rowSortingFeature,
columnFilteringFeature,
}),
_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 sharedOptions = tableOptions({
_features: tableFeatures({ rowSortingFeature, rowPaginationFeature }),
_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 Table composition Guide.
The enablePinning option has been split into separate options:
// v8
enablePinning: true
// v9
enableColumnPinning: true
enableRowPinning: true
All 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() |
| 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>[] = [...]
If you're using module augmentation to extend ColumnMeta, note that it now requires a TFeatures parameter.
The RowData type is now more restrictive.
Check out these examples to see v9 patterns in action: