Aggregation computes column values over sets of rows. It is independent from column grouping: use it for table totals, filtered totals, selected-row totals, or custom subsets without creating grouped rows. Register it alongside column grouping when grouped rows should also expose aggregate values.
Register rowAggregationFeature and the named definitions referenced by your columns. Registering individual built-ins keeps unused functions out of the bundle.
import {
rowAggregationFeature,
aggregationFn_mean,
aggregationFn_sum,
tableFeatures,
} from '@tanstack/table-core'
const features = tableFeatures({
rowAggregationFeature,
aggregationFns: {
mean: aggregationFn_mean,
sum: aggregationFn_sum,
},
})The full aggregationFns registry remains available for compatibility, but it bundles every built-in. Tables using stockFeatures already include rowAggregationFeature; they still need the definitions that named column options should resolve.
To combine aggregation with grouped rows, register both independent features:
const features = tableFeatures({
rowAggregationFeature,
columnGroupingFeature,
groupedRowModel: createGroupedRowModel(),
aggregationFns: { sum: aggregationFn_sum },
})A scalar aggregationFn can be a registered name, 'auto', or an inline definition.
const amountColumn = columnHelper.accessor('amount', {
aggregationFn: 'sum',
footer: ({ column }) => column.getAggregationValue<number>(),
})With no options argument, column.getAggregationValue() aggregates the table's pre-grouped row model. In the normal client pipeline this includes filtering, but precedes grouping, sorting, expansion, and pagination. It uses the column's maxAggregationDepth (0 by default).
Pass rows in the options object to override the default:
column.getAggregationValue({ rows: table.getCoreRowModel().rows }) // all core rows
column.getAggregationValue({ rows: table.getRowModel().rows }) // rendered model
column.getAggregationValue({
rows: table.getFilteredSelectedRowModel().rows,
})
column.getAggregationValue({ rows: customRows })
column.getAggregationValue({ rows: customRows, maxDepth: 1 })Depth is relative to the supplied row array. 0 selects those root rows, 1 selects their direct sub-rows, and so on. Selection returns a unique frontier: a branch that ends before the maximum depth contributes its deepest available row. Infinity selects terminal rows.
Set the cached default depth on the column:
const amountColumn = columnHelper.accessor('amount', {
aggregationFn: ['sum', 'mean', 'count'],
maxAggregationDepth: 0,
})Every aggregation configured on the column receives the same selected rows. Explicit-row calls are recomputed each time; the default call is cached against its row model, depth, registry, and column aggregation option.
table.getMaxSubRowDepth() returns the deepest structural depth in the core row model. To stop one level before the deepest sub-row frontier:
const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1)
column.getAggregationValue({
rows: table.getCoreRowModel().rows,
maxDepth,
})Use an array to return a keyed result object. Registered names become their own keys. Use a descriptor when the result needs a different key or when an inline definition appears in the array.
const scoreColumn = columnHelper.accessor('score', {
aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }],
footer: ({ column }) => {
const result = column.getAggregationValue<{
count: number
mean: number | undefined
range: [number | undefined, number | undefined]
}>()
return `${result.count} values; mean ${result.mean}; range ${result.range}`
},
})Every entry needs a unique stable id. Duplicate ids, missing descriptor ids, and unregistered names warn in development and preserve the affected key with an undefined value.
Custom aggregations are context-based definitions. rows contains the unique frontier selected at maxDepth, and getValue(row) reads the current column's value.
const joined = constructAggregationFn<any, any, string, string>({
aggregate: ({ rows, getValue }) =>
rows
.map((row) => getValue(row))
.filter(Boolean)
.join(', '),
})The context also includes column, columnId, maxDepth, and table. During grouped aggregation it includes groupingRow and subRows; root and caller-supplied-row aggregation omit those properties. The grouping depth is groupingRow.depth. subRows contains the immediate rows at that grouping level, so an aggregation can explicitly choose immediate sub-rows instead of the depth-selected rows:
const subRowCount = constructAggregationFn<any, any, unknown, number>({
aggregate: ({ subRows, rows }) => (subRows ?? rows).length,
})At the terminal grouping level, subRows contains direct data rows. At a nested level, it contains the immediate synthetic sub-row groups.
All built-in aggregation definitions consume the same depth-selected rows. subRows remains available when a custom definition intentionally needs the grouping row's immediate structural children.
For a result that can be combined more efficiently from already-computed sub-row results, provide a merge function:
const sum = constructAggregationFn<any, any, unknown, number>({
aggregate: ({ rows, getValue }) =>
rows.reduce((total, row) => {
const value = getValue(row)
return total + (typeof value === 'number' ? value : 0)
}, 0),
merge: ({ subRowResults }) =>
subRowResults.reduce((total, value) => total + value, 0),
})For merge, subRowResults[i] is the aggregation result previously computed for subRows[i].
Without merge, nested grouping calls aggregate with both the group's depth-selected rows and its immediate subRows. This replaces the previous callable aggregation signature and its fromRows and resolveDataValue properties while preserving the ability to choose either row set.
aggregatedCell renders aggregate values on synthetic grouped rows.
const amountColumn = columnHelper.accessor('amount', {
aggregationFn: 'sum',
aggregatedCell: ({ getValue }) => getValue<number>().toLocaleString(),
})Use cell.getIsAggregated() to distinguish these cells. Grouping-only tables do not expose this method; it belongs to rowAggregationFeature.
A column can handle aggregation-value requests before local calculation:
const amountColumn = columnHelper.accessor('amount', {
aggregationFn: 'sum',
getAggregationValue: ({ rows }) => {
if (rows !== undefined) return undefined // use local fallback for overrides
return { value: serverTotals.amount }
},
})Returning { value } marks the request as handled, including { value: undefined }. Returning undefined uses the local fallback. Put the same provider on defaultColumn to share it across columns.
Set manualAggregation: true to disable the local fallback for column.getAggregationValue(). This is separate from manualGrouping, which controls whether the grouped row model runs.
aggregationFn: 'auto' inspects the first core row value. Numbers resolve to a registered sum, Dates resolve to a registered extent, and other values do not resolve an aggregation.
Worker-backed grouped row models eagerly compute explicitly configured grouped aggregates in the worker. column.getAggregationValue() still executes its final total on the main thread over the selected row model. Aggregation results crossing the worker boundary must be structured-cloneable.
Every adapter has a standalone example showing grand totals, multiple aggregations, and pagination:
Every framework adapter also has a grouped aggregation example combining the same API with grouped rows and grand-total footers: