diff --git a/examples/react/column-resizing-performant/src/main.tsx b/examples/react/column-resizing-performant/src/main.tsx
index 8d3335ac1a..3f9692ab4e 100644
--- a/examples/react/column-resizing-performant/src/main.tsx
+++ b/examples/react/column-resizing-performant/src/main.tsx
@@ -119,7 +119,7 @@ function App() {
Regenerate Data
diff --git a/examples/vue/virtualized-infinite-scrolling/package.json b/examples/vue/virtualized-infinite-scrolling/package.json
index 0da266d956..f096657f13 100644
--- a/examples/vue/virtualized-infinite-scrolling/package.json
+++ b/examples/vue/virtualized-infinite-scrolling/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"@faker-js/faker": "^10.4.0",
+ "@tanstack/match-sorter-utils": "^9.0.0-alpha.4",
"@tanstack/vue-query": "^5.100.10",
"@tanstack/vue-store": "^0.11.0",
"@tanstack/vue-table": "^9.0.0-alpha.45",
diff --git a/packages/table-core/src/features/column-ordering/columnOrderingFeature.ts b/packages/table-core/src/features/column-ordering/columnOrderingFeature.ts
index e6cde63817..5014f5a1de 100644
--- a/packages/table-core/src/features/column-ordering/columnOrderingFeature.ts
+++ b/packages/table-core/src/features/column-ordering/columnOrderingFeature.ts
@@ -63,6 +63,7 @@ export function constructColumnOrderingFeature<
column.table.atoms.columnOrder?.get(),
column.table.atoms.columnPinning?.get(),
column.table.atoms.grouping?.get(),
+ column.table.atoms.columnVisibility?.get(),
],
},
column_getIsFirstColumn: {
diff --git a/packages/table-core/src/features/column-pinning/columnPinningFeature.ts b/packages/table-core/src/features/column-pinning/columnPinningFeature.ts
index b399295894..5ef3961929 100644
--- a/packages/table-core/src/features/column-pinning/columnPinningFeature.ts
+++ b/packages/table-core/src/features/column-pinning/columnPinningFeature.ts
@@ -301,11 +301,7 @@ export function constructColumnPinningFeature<
},
table_getPinnedLeafColumns: {
fn: (position) => table_getPinnedLeafColumns(table, position),
- memoDeps: (position) => [
- position,
- table.options.columns,
- table.atoms.columnPinning?.get(),
- ],
+ // must not memo here as it's just a shortcut function
},
// visible leaf columns
table_getLeftVisibleLeafColumns: {
@@ -334,12 +330,7 @@ export function constructColumnPinningFeature<
},
table_getPinnedVisibleLeafColumns: {
fn: (position) => table_getPinnedVisibleLeafColumns(table, position),
- memoDeps: (position) => [
- position,
- table.options.columns,
- table.atoms.columnPinning?.get(),
- table.atoms.columnVisibility?.get(),
- ],
+ // must not memo here as it's just a shortcut function
},
})
},
diff --git a/packages/table-core/src/features/column-sizing/columnSizingFeature.ts b/packages/table-core/src/features/column-sizing/columnSizingFeature.ts
index 964c1c011b..e054520bec 100644
--- a/packages/table-core/src/features/column-sizing/columnSizingFeature.ts
+++ b/packages/table-core/src/features/column-sizing/columnSizingFeature.ts
@@ -1,10 +1,8 @@
import {
assignPrototypeAPIs,
assignTableAPIs,
- callMemoOrStaticFn,
makeStateUpdater,
} from '../../utils'
-import { table_getPinnedVisibleLeafColumns } from '../column-pinning/columnPinningFeature.utils'
import {
column_getAfter,
column_getSize,
@@ -75,31 +73,31 @@ export function constructColumnSizingFeature<
assignPrototypeAPIs('columnSizingFeature', prototype, table, {
column_getSize: {
fn: (column) => column_getSize(column),
+ memoDeps: (column) => [
+ table.options.columns,
+ table.atoms.columnSizing?.get()?.[column.id], // just this column's size state
+ ],
},
column_getStart: {
fn: (column, position) => column_getStart(column, position),
memoDeps: (column, position) => [
position,
- callMemoOrStaticFn(
- column.table,
- 'getPinnedVisibleLeafColumns',
- table_getPinnedVisibleLeafColumns,
- position,
- ),
- column.table.atoms.columnSizing?.get(),
+ table.options.columns,
+ table.atoms.columnSizing?.get(),
+ table.atoms.columnOrder?.get(),
+ table.atoms.columnPinning?.get(),
+ table.atoms.columnVisibility?.get(),
],
},
column_getAfter: {
fn: (column, position) => column_getAfter(column, position),
memoDeps: (column, position) => [
position,
- callMemoOrStaticFn(
- column.table,
- 'getPinnedVisibleLeafColumns',
- table_getPinnedVisibleLeafColumns,
- position,
- ),
- column.table.atoms.columnSizing?.get(),
+ table.options.columns,
+ table.atoms.columnSizing?.get(),
+ table.atoms.columnOrder?.get(),
+ table.atoms.columnPinning?.get(),
+ table.atoms.columnVisibility?.get(),
],
},
column_resetSize: {
@@ -112,9 +110,23 @@ export function constructColumnSizingFeature<
assignPrototypeAPIs('columnSizingFeature', prototype, table, {
header_getSize: {
fn: (header) => header_getSize(header),
+ memoDeps: (header) => [
+ table.options.columns,
+ header.column.columns.length > 0
+ ? table.atoms.columnSizing?.get() // must be all columns (sum child columns)
+ : table.atoms.columnSizing?.get()?.[header.column.id], // can just check it's associated column size state
+ ],
},
header_getStart: {
fn: (header) => header_getStart(header),
+ memoDeps: (header, position) => [
+ position,
+ table.options.columns,
+ table.atoms.columnSizing?.get(),
+ table.atoms.columnOrder?.get(),
+ table.atoms.columnPinning?.get(),
+ table.atoms.columnVisibility?.get(),
+ ],
},
})
},
diff --git a/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts b/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts
index ba3384af33..4ac65058eb 100644
--- a/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts
+++ b/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts
@@ -91,6 +91,14 @@ export function column_getStart<
column: Column_Internal,
position: ColumnPinningPosition | 'center',
): number {
+ const index = callMemoOrStaticFn(
+ column,
+ 'getIndex',
+ column_getIndex,
+ position,
+ )
+ if (index <= 0) return 0
+
const visibleLeafColumns = callMemoOrStaticFn(
column.table,
'getPinnedVisibleLeafColumns',
@@ -98,9 +106,11 @@ export function column_getStart<
position,
)
- return visibleLeafColumns
- .slice(0, callMemoOrStaticFn(column, 'getIndex', column_getIndex, position))
- .reduce((sum: number, c) => sum + column_getSize(c), 0)
+ const prevColumn = visibleLeafColumns[index - 1]!
+ return (
+ callMemoOrStaticFn(prevColumn, 'getStart', column_getStart, position) +
+ callMemoOrStaticFn(prevColumn, 'getSize', column_getSize)
+ )
}
/**
@@ -127,12 +137,19 @@ export function column_getAfter<
table_getPinnedVisibleLeafColumns,
position,
)
+ const index = callMemoOrStaticFn(
+ column,
+ 'getIndex',
+ column_getIndex,
+ position,
+ )
+ if (index < 0 || index >= visibleLeafColumns.length - 1) return 0
- return visibleLeafColumns
- .slice(
- callMemoOrStaticFn(column, 'getIndex', column_getIndex, position) + 1,
- )
- .reduce((sum: number, c) => sum + column_getSize(c), 0)
+ const nextColumn = visibleLeafColumns[index + 1]!
+ return (
+ callMemoOrStaticFn(nextColumn, 'getSize', column_getSize) +
+ callMemoOrStaticFn(nextColumn, 'getAfter', column_getAfter, position)
+ )
}
/**
@@ -204,7 +221,8 @@ export function header_getStart<
const prevSiblingHeader = header.headerGroup?.headers[header.index - 1]
if (prevSiblingHeader) {
return (
- header_getStart(prevSiblingHeader) + header_getSize(prevSiblingHeader)
+ callMemoOrStaticFn(prevSiblingHeader, 'getStart', header_getStart) +
+ callMemoOrStaticFn(prevSiblingHeader, 'getSize', header_getSize)
)
}
}
diff --git a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts
index 40de15b8ee..b7da03f59c 100644
--- a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts
+++ b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts
@@ -70,8 +70,8 @@ export function constructColumnVisibilityFeature<
column_getIsVisible: {
fn: (column) => column_getIsVisible(column),
memoDeps: (column) => [
- column.table.options.columns,
- column.table.atoms.columnVisibility?.get(),
+ table.options.columns,
+ table.atoms.columnVisibility?.get(),
column.columns,
],
},
@@ -93,8 +93,8 @@ export function constructColumnVisibilityFeature<
fn: (row) => row_getVisibleCells(row),
memoDeps: (row) => [
row.getAllCells(),
- row.table.atoms.columnPinning?.get(),
- row.table.atoms.columnVisibility?.get(),
+ table.atoms.columnPinning?.get(),
+ table.atoms.columnVisibility?.get(),
],
},
})
diff --git a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts
index f550db70b7..f9c2e9a03a 100644
--- a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts
+++ b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts
@@ -139,19 +139,30 @@ export function row_getVisibleCells<
row.table.atoms.columnPinning?.get() ?? getDefaultColumnPinningState()
if (!left.length && !right.length) return cells // no pinning, return early
- // re-order cells for column pinning
- const leftCells = []
- const rightCells = []
- const centerCells = []
+ const cellsByColumnId = new Map>()
+ for (const cell of cells) cellsByColumnId.set(cell.column.id, cell)
+
+ const leftCells: Array> = []
+ for (const columnId of left) {
+ const cell = cellsByColumnId.get(columnId)
+ if (cell) leftCells.push(cell)
+ }
+
+ const rightCells: Array> = []
+ for (const columnId of right) {
+ const cell = cellsByColumnId.get(columnId)
+ if (cell) rightCells.push(cell)
+ }
+
+ // Center cells: visible cells in natural column order, minus pinned ones.
+ const leftSet = new Set(left)
+ const rightSet = new Set(right)
+ const centerCells: Array> = []
for (const cell of cells) {
- if (left.includes(cell.column.id)) {
- leftCells.push(cell)
- } else if (right.includes(cell.column.id)) {
- rightCells.push(cell)
- } else {
- centerCells.push(cell)
- }
+ const id = cell.column.id
+ if (!leftSet.has(id) && !rightSet.has(id)) centerCells.push(cell)
}
+
return [...leftCells, ...centerCells, ...rightCells]
}
diff --git a/packages/table-core/tests/unit/features/column-sizing/columnSizingFeature.utils.test.ts b/packages/table-core/tests/unit/features/column-sizing/columnSizingFeature.utils.test.ts
new file mode 100644
index 0000000000..eecd6e7bb5
--- /dev/null
+++ b/packages/table-core/tests/unit/features/column-sizing/columnSizingFeature.utils.test.ts
@@ -0,0 +1,324 @@
+import { describe, expect, it } from 'vitest'
+import {
+ columnSizingFeature,
+ constructTable,
+ coreFeatures,
+ createCoreRowModel,
+} from '../../../../src'
+import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings'
+
+const _features = {
+ ...coreFeatures,
+ columnSizingFeature,
+ coreReativityFeature: storeReactivityBindings(),
+}
+
+type Item = { id: string; a: string; b: string; c: string; d: string }
+
+const data: Array- = [
+ { id: '1', a: 'a1', b: 'b1', c: 'c1', d: 'd1' },
+ { id: '2', a: 'a2', b: 'b2', c: 'c2', d: 'd2' },
+]
+
+function makeTable(opts: {
+ columns: Array
+ columnSizing?: Record
+}): any {
+ return constructTable({
+ _features,
+ _rowModels: { coreRowModel: createCoreRowModel() },
+ columns: opts.columns,
+ data,
+ state: opts.columnSizing ? { columnSizing: opts.columnSizing } : undefined,
+ } as any)
+}
+
+describe('header_getSize', () => {
+ it('returns default size for a leaf header', () => {
+ const table = makeTable({
+ columns: [{ id: 'a', accessorKey: 'a' }],
+ })
+ const header = table.getHeaderGroups()[0].headers[0]
+ expect(header.getSize()).toBe(150)
+ })
+
+ it('returns columnDef.size for a leaf header', () => {
+ const table = makeTable({
+ columns: [{ id: 'a', accessorKey: 'a', size: 200 }],
+ })
+ const header = table.getHeaderGroups()[0].headers[0]
+ expect(header.getSize()).toBe(200)
+ })
+
+ it('returns sum of subHeader sizes for a parent header', () => {
+ const table = makeTable({
+ columns: [
+ {
+ id: 'group',
+ header: 'Group',
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ },
+ ],
+ })
+ const groupRow = table.getHeaderGroups()[0]
+ const groupHeader = groupRow.headers[0]
+ expect(groupHeader.getSize()).toBe(300)
+ })
+
+ it('respects columnSizing state', () => {
+ const table = makeTable({
+ columns: [{ id: 'a', accessorKey: 'a', size: 100 }],
+ columnSizing: { a: 250 },
+ })
+ const header = table.getHeaderGroups()[0].headers[0]
+ expect(header.getSize()).toBe(250)
+ })
+})
+
+describe('header_getStart', () => {
+ it('returns 0 for the first header', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ const headers = table.getHeaderGroups()[0].headers
+ expect(headers[0].getStart()).toBe(0)
+ })
+
+ it('returns size of preceding header for the second header', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ const headers = table.getHeaderGroups()[0].headers
+ expect(headers[1].getStart()).toBe(100)
+ })
+
+ it('returns running sum of preceding sizes', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ { id: 'c', accessorKey: 'c', size: 50 },
+ { id: 'd', accessorKey: 'd', size: 75 },
+ ],
+ })
+ const headers = table.getHeaderGroups()[0].headers
+ expect(headers[0].getStart()).toBe(0)
+ expect(headers[1].getStart()).toBe(100)
+ expect(headers[2].getStart()).toBe(300)
+ expect(headers[3].getStart()).toBe(350)
+ })
+
+ it('respects columnSizing state', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ columnSizing: { a: 75 },
+ })
+ const headers = table.getHeaderGroups()[0].headers
+ expect(headers[1].getStart()).toBe(75)
+ })
+
+ it('updates getStart when columnSizing changes (memo invalidation)', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ let headers = table.getHeaderGroups()[0].headers
+ expect(headers[1].getStart()).toBe(100)
+
+ table.setColumnSizing({ a: 500 })
+ headers = table.getHeaderGroups()[0].headers
+ expect(headers[1].getStart()).toBe(500)
+ })
+
+ it('returns running sum across nested header groups (parent row)', () => {
+ const table = makeTable({
+ columns: [
+ {
+ id: 'g1',
+ header: 'g1',
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ },
+ {
+ id: 'g2',
+ header: 'g2',
+ columns: [
+ { id: 'c', accessorKey: 'c', size: 50 },
+ { id: 'd', accessorKey: 'd', size: 75 },
+ ],
+ },
+ ],
+ })
+ const groups = table.getHeaderGroups()
+ const parentRow = groups[0].headers
+ expect(parentRow[0].getStart()).toBe(0)
+ // group 2 starts after group 1 (100 + 200)
+ expect(parentRow[1].getStart()).toBe(300)
+
+ const leafRow = groups[1].headers
+ expect(leafRow[0].getStart()).toBe(0)
+ expect(leafRow[1].getStart()).toBe(100)
+ expect(leafRow[2].getStart()).toBe(300)
+ expect(leafRow[3].getStart()).toBe(350)
+ })
+
+ it('returns 0 for a single-header group', () => {
+ const table = makeTable({
+ columns: [{ id: 'a', accessorKey: 'a', size: 100 }],
+ })
+ const headers = table.getHeaderGroups()[0].headers
+ expect(headers[0].getStart()).toBe(0)
+ })
+})
+
+describe('column_getStart', () => {
+ it('returns 0 for the first column', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ const cols = table.getAllLeafColumns()
+ expect(cols[0].getStart()).toBe(0)
+ })
+
+ it('returns size of preceding column for the second column', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ const cols = table.getAllLeafColumns()
+ expect(cols[1].getStart()).toBe(100)
+ })
+
+ it('returns running sum of preceding column sizes', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ { id: 'c', accessorKey: 'c', size: 50 },
+ { id: 'd', accessorKey: 'd', size: 75 },
+ ],
+ })
+ const cols = table.getAllLeafColumns()
+ expect(cols[0].getStart()).toBe(0)
+ expect(cols[1].getStart()).toBe(100)
+ expect(cols[2].getStart()).toBe(300)
+ expect(cols[3].getStart()).toBe(350)
+ })
+
+ it('respects columnSizing state', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ { id: 'c', accessorKey: 'c', size: 50 },
+ ],
+ columnSizing: { a: 75, b: 30 },
+ })
+ const cols = table.getAllLeafColumns()
+ expect(cols[2].getStart()).toBe(105)
+ })
+
+ it('updates getStart when columnSizing changes (memo invalidation)', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ let cols = table.getAllLeafColumns()
+ expect(cols[1].getStart()).toBe(100)
+
+ table.setColumnSizing({ a: 500 })
+ cols = table.getAllLeafColumns()
+ expect(cols[1].getStart()).toBe(500)
+ })
+})
+
+describe('column_getAfter', () => {
+ it('returns 0 for the last column', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ const cols = table.getAllLeafColumns()
+ expect(cols[1].getAfter()).toBe(0)
+ })
+
+ it('returns size of following column for the second-to-last column', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ const cols = table.getAllLeafColumns()
+ expect(cols[0].getAfter()).toBe(200)
+ })
+
+ it('returns running sum of following column sizes', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ { id: 'c', accessorKey: 'c', size: 50 },
+ { id: 'd', accessorKey: 'd', size: 75 },
+ ],
+ })
+ const cols = table.getAllLeafColumns()
+ expect(cols[0].getAfter()).toBe(325)
+ expect(cols[1].getAfter()).toBe(125)
+ expect(cols[2].getAfter()).toBe(75)
+ expect(cols[3].getAfter()).toBe(0)
+ })
+
+ it('respects columnSizing state', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ { id: 'c', accessorKey: 'c', size: 50 },
+ ],
+ columnSizing: { b: 30, c: 25 },
+ })
+ const cols = table.getAllLeafColumns()
+ expect(cols[0].getAfter()).toBe(55)
+ })
+
+ it('updates getAfter when columnSizing changes (memo invalidation)', () => {
+ const table = makeTable({
+ columns: [
+ { id: 'a', accessorKey: 'a', size: 100 },
+ { id: 'b', accessorKey: 'b', size: 200 },
+ ],
+ })
+ let cols = table.getAllLeafColumns()
+ expect(cols[0].getAfter()).toBe(200)
+
+ table.setColumnSizing({ b: 500 })
+ cols = table.getAllLeafColumns()
+ expect(cols[0].getAfter()).toBe(500)
+ })
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cc5e25908b..787cd89e4a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7353,6 +7353,9 @@ importers:
'@faker-js/faker':
specifier: ^10.4.0
version: 10.4.0
+ '@tanstack/match-sorter-utils':
+ specifier: workspace:*
+ version: link:../../../packages/match-sorter-utils
'@tanstack/vue-query':
specifier: ^5.100.10
version: 5.100.10(vue@3.5.34(typescript@6.0.3))
| | |