From e47ce85427b03b85a70f9eef64a06e699acc7e45 Mon Sep 17 00:00:00 2001 From: ATOM00blue <219721791+ATOM00blue@users.noreply.github.com> Date: Fri, 22 May 2026 07:16:01 +0530 Subject: [PATCH] fix(grouping): aggregate grouped columns at shallower group levels When grouping by multiple columns, a grouped column was never aggregated on any group row because `getValue` skipped aggregation whenever the column appeared anywhere in the grouping state. As a result a column that is grouped at a deeper level showed the first leaf row's raw value at its ancestor group rows instead of the aggregated value. Only treat a column as a grouping value when it groups the row at the current depth or a shallower (ancestor) level. Columns grouped at a deeper level are aggregated as expected. --- .changeset/fix-grouped-column-aggregation.md | 5 ++ .../src/utils/getGroupedRowModel.ts | 8 ++- .../tests/getGroupedRowModel.test.ts | 50 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-grouped-column-aggregation.md diff --git a/.changeset/fix-grouped-column-aggregation.md b/.changeset/fix-grouped-column-aggregation.md new file mode 100644 index 0000000000..c63f910eb3 --- /dev/null +++ b/.changeset/fix-grouped-column-aggregation.md @@ -0,0 +1,5 @@ +--- +'@tanstack/table-core': patch +--- + +fix(grouping): aggregate grouped columns at shallower group levels instead of showing the first row's value diff --git a/packages/table-core/src/utils/getGroupedRowModel.ts b/packages/table-core/src/utils/getGroupedRowModel.ts index 33267bcaa1..cf2a577871 100644 --- a/packages/table-core/src/utils/getGroupedRowModel.ts +++ b/packages/table-core/src/utils/getGroupedRowModel.ts @@ -92,8 +92,12 @@ export function getGroupedRowModel(): ( subRows, leafRows, getValue: (columnId: string) => { - // Don't aggregate columns that are in the grouping - if (existingGrouping.includes(columnId)) { + // Don't aggregate columns that group this row at the current + // level or an ancestor level - their value is constant across + // the group. Columns grouped at a deeper level still need to + // be aggregated here. + const groupingIndex = existingGrouping.indexOf(columnId) + if (groupingIndex > -1 && groupingIndex <= depth) { if (row._valuesCache.hasOwnProperty(columnId)) { return row._valuesCache[columnId] } diff --git a/packages/table-core/tests/getGroupedRowModel.test.ts b/packages/table-core/tests/getGroupedRowModel.test.ts index 16e3632588..66445febf8 100644 --- a/packages/table-core/tests/getGroupedRowModel.test.ts +++ b/packages/table-core/tests/getGroupedRowModel.test.ts @@ -5,6 +5,17 @@ import { createTable } from '../src/core/table' import { getGroupedRowModel } from '../src/utils/getGroupedRowModel' import { makeData, Person } from './makeTestData' +function createPerson(firstName: string, age: number): Person { + return { + firstName, + lastName: 'Doe', + age, + visits: 0, + progress: 0, + status: 'single', + } +} + type personKeys = keyof Person type PersonColumn = ColumnDef @@ -49,4 +60,43 @@ describe('#getGroupedRowModel', () => { ).toEqual(50000) expect(end.valueOf() - start.valueOf()).toBeLessThan(5000) }) + + it('aggregates a secondary grouped column at parent group levels', () => { + const data: Person[] = [ + // first Engineering row is intentionally not the min, so the bug + // (returning the first row's raw value) differs from the aggregate + createPerson('Engineering', 30), + createPerson('Engineering', 24), + createPerson('Sales', 40), + ] + + const columnHelper = createColumnHelper() + const columns = [ + columnHelper.accessor('firstName', { id: 'firstName' }), + columnHelper.accessor('age', { id: 'age', aggregationFn: 'min' }), + ] + + const table = createTable({ + onStateChange() {}, + renderFallbackValue: '', + data, + state: { grouping: ['firstName', 'age'] }, + columns, + getCoreRowModel: getCoreRowModel(), + getGroupedRowModel: getGroupedRowModel(), + }) + + const rowsById = table.getGroupedRowModel().rowsById + const engineering = rowsById['firstName:Engineering'] + + // `age` is grouped at a deeper level, so the Engineering group row should + // still expose the aggregated (min) age across its leaf rows. + expect(engineering?.getValue('age')).toBe(24) + expect(rowsById['firstName:Sales']?.getValue('age')).toBe(40) + + // at the `age` group level the column is the grouping key, so it keeps the + // grouping value rather than aggregating + expect(rowsById['firstName:Engineering>age:24']?.getValue('age')).toBe(24) + expect(rowsById['firstName:Engineering>age:30']?.getValue('age')).toBe(30) + }) })