| name | customizing-feature-behavior |
| description | Override per-column `sortFn`, `filterFn`, `aggregationFn` and table-level `globalFilterFn` in TanStack Table v9. Covers built-in `filterFns` / `sortFns` / `aggregationFns` registries (passed to `createFilteredRowModel(filterFns)` / `createSortedRowModel(sortFns)` / `createGroupedRowModel(aggregationFns)`), authoring custom functions with the `FilterFn` / `SortFn` / `AggregationFn` signatures, chaining filter→sort via the `addMeta` callback + `row.columnFiltersMeta`, `resolveFilterValue`, `autoRemove`, `invertSorting`, `sortUndefined` ('first'|'last'|-1|1), and `sortDescFirst`. Distinguishes `aggregationFn` (produces value) from `aggregatedCell` (renders value).
|
| type | core |
| library | tanstack-table |
| library_version | 9.0.0-alpha.48 |
| requires | ["state-management"] |
| sources | ["TanStack/table:docs/guide/sorting.md","TanStack/table:docs/guide/column-filtering.md","TanStack/table:docs/guide/fuzzy-filtering.md","TanStack/table:packages/table-core/src/fns/filterFns.ts","TanStack/table:packages/table-core/src/fns/sortFns.ts","TanStack/table:packages/table-core/src/fns/aggregationFns.ts","TanStack/table:examples/react/filters-fuzzy/src/main.tsx"] |
This skill builds on tanstack-table/state-management. Read it first for how feature plugins drive state slices.
Setup
v9 customization happens in three places:
- Built-in function registries —
filterFns, sortFns, aggregationFns — passed as arguments to row-model factories so unused fns tree-shake away.
- Per-column overrides —
columnDef.filterFn, columnDef.sortFn, columnDef.aggregationFn (string name OR inline function).
- Table-level overrides —
tableOptions.globalFilterFn.
import {
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
globalFilteringFeature,
columnGroupingFeature,
rowExpandingFeature,
createFilteredRowModel,
createSortedRowModel,
createGroupedRowModel,
createExpandedRowModel,
filterFns,
sortFns,
aggregationFns,
createColumnHelper,
} from '@tanstack/table-core'
import type { FilterFn, SortFn, AggregationFn } from '@tanstack/table-core'
import {
rankItem,
compareItems,
type RankingInfo,
} from '@tanstack/match-sorter-utils'
type Person = {
id: string
firstName: string
lastName: string
revenue: number
status: 'single' | 'complicated' | 'relationship'
}
const _features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
globalFilteringFeature,
columnGroupingFeature,
rowExpandingFeature,
})
declare module '@tanstack/table-core' {
interface FilterFns {
fuzzy: FilterFn<typeof _features, Person>
}
interface FilterMeta {
itemRank?: RankingInfo
}
}
const fuzzyFilter: FilterFn<typeof _features, Person> = (
row,
columnId,
value,
addMeta,
) => {
const itemRank = rankItem(row.getValue(columnId), value)
addMeta?.({ itemRank })
return itemRank.passed
}
const columnHelper = createColumnHelper<typeof _features, Person>()
const columns = columnHelper.columns([
columnHelper.accessor('firstName', {
filterFn: 'fuzzy',
sortFn: 'alphanumeric',
}),
columnHelper.accessor('revenue', {
aggregationFn: 'sum',
aggregatedCell: (info) => `$${info.getValue<number>().toLocaleString()}`,
}),
])
const table = constructTable({
_features,
_rowModels: {
filteredRowModel: createFilteredRowModel({
...filterFns,
fuzzy: fuzzyFilter,
}),
sortedRowModel: createSortedRowModel(sortFns),
groupedRowModel: createGroupedRowModel(aggregationFns),
expandedRowModel: createExpandedRowModel(),
},
columns,
data,
globalFilterFn: 'fuzzy',
})
Core Patterns
Pick a built-in sortFn by name + direction control
columnHelper.accessor('lastName', {
sortFn: 'alphanumeric',
sortDescFirst: false,
sortUndefined: 'last',
})
Layered direction controls:
sortDescFirst: true/false — first click sorts descending
sortUndefined: 'first' | 'last' | -1 | 1 | false — string forms are absolute; numeric flips with desc
invertSorting: true — for "lower-is-better" scales (rank 1 above rank 2 even when descending)
Filter → sort handoff via addMeta
const fuzzyFilter: FilterFn<typeof _features, Person> = (
row,
columnId,
value,
addMeta,
) => {
const itemRank = rankItem(row.getValue(columnId), value)
addMeta?.({ itemRank })
return itemRank.passed
}
const fuzzySort: SortFn<typeof _features, Person> = (rowA, rowB, columnId) => {
let dir = 0
if (rowA.columnFiltersMeta[columnId]) {
dir = compareItems(
rowA.columnFiltersMeta[columnId].itemRank!,
rowB.columnFiltersMeta[columnId].itemRank!,
)
}
return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir
}
columnHelper.accessor('fullName', { filterFn: 'fuzzy', sortFn: fuzzySort })
row.columnFiltersMeta is keyed by the column id that produced the meta (or '__global__' for the global filter). The sortFn MUST look up the same column id its filterFn used.
Custom aggregationFn for grouping
import type { AggregationFn } from '@tanstack/table-core'
const weightedMean: AggregationFn<typeof _features, Person> = (
columnId,
leafRows,
) => {
let totalWeight = 0
let weightedSum = 0
leafRows.forEach((row) => {
const v = row.getValue<number>(columnId)
const w = row.original.revenue
weightedSum += v * w
totalWeight += w
})
return totalWeight === 0 ? 0 : weightedSum / totalWeight
}
const table = constructTable({
_features,
_rowModels: {
groupedRowModel: createGroupedRowModel({ ...aggregationFns, weightedMean }),
expandedRowModel: createExpandedRowModel(),
},
columns: columnHelper.columns([
columnHelper.accessor('revenue', {
aggregationFn: 'weightedMean',
aggregatedCell: (info) => `$${info.getValue<number>().toFixed(2)}`,
}),
]),
data,
})
Common Mistakes
[CRITICAL] Referencing a custom filterFn by string without registering it
Wrong:
const table = useTable({
_features,
columns: [columnHelper.accessor('fullName', { filterFn: 'fuzzy' })],
_rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
},
data,
})
Correct:
declare module '@tanstack/react-table' {
interface FilterFns {
fuzzy: FilterFn<typeof _features, Person>
}
}
const fuzzyFilter: FilterFn<typeof _features, Person> = (
row,
columnId,
value,
addMeta,
) => {
const itemRank = rankItem(row.getValue(columnId), value)
addMeta?.({ itemRank })
return itemRank.passed
}
const table = useTable({
_features,
columns: [columnHelper.accessor('fullName', { filterFn: 'fuzzy' })],
_rowModels: {
filteredRowModel: createFilteredRowModel({
...filterFns,
fuzzy: fuzzyFilter,
}),
},
data,
})
String values are looked up in table._rowModelFns.filterFns. Unregistered names log Could not find a valid 'column.filterFn' … in dev and silently no-op in prod.
Source: examples/react/filters-fuzzy/src/main.tsx; packages/table-core/src/features/column-filtering/columnFilteringFeature.utils.ts
[HIGH] Using v8 sortingFn / sortingFns names
Wrong:
columnHelper.accessor('age', {
sortingFn: 'alphanumeric',
})
Correct:
columnHelper.accessor('age', {
sortFn: 'alphanumeric',
})
v9 renamed every sorting API: sortingFn → sortFn, sortingFns → sortFns, type SortingFn → SortFn, column.getSortingFn() → column.getSortFn(). The default sortFn is 'auto', falling back to sortFn_basic if the lookup misses — so wrong names sort wrong instead of erroring.
Source: docs/framework/react/guide/migrating.md; packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts
[HIGH] Custom sortFn reads filter meta from a different column id
Wrong:
const fuzzySort: SortFn<typeof _features, Person> = (a, b, columnId) => {
const meta = a.columnFiltersMeta['firstName']
return meta
? compareItems(meta.itemRank, b.columnFiltersMeta['firstName'].itemRank)
: 0
}
columnHelper.accessor('fullName', { filterFn: 'fuzzy', sortFn: fuzzySort })
Correct:
const fuzzySort: SortFn<typeof _features, Person> = (rowA, rowB, columnId) => {
let dir = 0
if (rowA.columnFiltersMeta[columnId]) {
dir = compareItems(
rowA.columnFiltersMeta[columnId].itemRank!,
rowB.columnFiltersMeta[columnId].itemRank!,
)
}
return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir
}
row.columnFiltersMeta is keyed by the column id that produced it. Always use the columnId argument the sortFn receives.
Source: examples/react/filters-fuzzy/src/main.tsx
[MEDIUM] Returning a complex value from the accessor while using a built-in sortFn
Wrong:
columnHelper.accessor((row) => row.name, {
id: 'name',
sortFn: 'alphanumeric',
})
Correct:
columnHelper.accessor((row) => `${row.name.first} ${row.name.last}`, {
id: 'fullName',
sortFn: 'alphanumeric',
})
columnHelper.accessor((row) => row.name, {
id: 'name',
sortFn: (a, b, id) => {
const av = a.getValue<{ first: string }>(id).first
const bv = b.getValue<{ first: string }>(id).first
return av === bv ? 0 : av > bv ? 1 : -1
},
})
Built-in sortFns (alphanumeric, text, basic) coerce via comparison operators. Object accessors collapse to "[object Object]" and every row ties.
Source: packages/table-core/src/fns/sortFns.ts
[MEDIUM] Confusing aggregationFn with aggregatedCell
Wrong:
columnHelper.accessor('revenue', {
aggregationFn: (id, leaves) => <b>${leaves.reduce((a, r) => a + r.getValue(id), 0)}</b>,
})
Correct:
columnHelper.accessor('revenue', {
aggregationFn: 'sum',
aggregatedCell: (info) => <b>${info.getValue<number>().toLocaleString()}</b>, // renders it
})
aggregationFn produces the grouped-row value (signature (columnId, leafRows, childRows)). aggregatedCell renders it. Don't combine.
Source: packages/table-core/src/features/column-grouping/columnGroupingFeature.types.ts
[CRITICAL] Reimplementing what built-in APIs provide
Wrong:
const [sorting, setSorting] = useState([])
const sortedData = useMemo(() => [...data].sort(), [data, sorting])
Correct:
const table = useTable({
_features: tableFeatures({ rowSortingFeature }),
_rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
columns,
data,
})
Source: maintainer interview (Phase 4, 2026-05-17)
See also
tanstack-table/filtering — filterFn placement, fuzzy filter pattern, faceted UI
tanstack-table/sorting — built-in sortFns, multi-sort, sortUndefined
tanstack-table/grouping — aggregationFn signature details and built-ins