Skip to content

Commit e6018a9

Browse files
committed
feat: change to prototype functions for row features
Inspired by TanStack#5927
1 parent 02c203a commit e6018a9

10 files changed

Lines changed: 438 additions & 348 deletions

File tree

packages/table-core/src/core/row.ts

Lines changed: 103 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface CoreRow<TData extends RowData> {
66
_getAllCellsByColumnId: () => Record<string, Cell<TData, unknown>>
77
_uniqueValuesCache: Record<string, unknown>
88
_valuesCache: Record<string, unknown>
9+
clone: () => Row<TData>
910
/**
1011
* The depth of the row (if nested or grouped) relative to the root row array.
1112
* @link [API Docs](https://tanstack.com/table/v8/docs/api/core/row#depth)
@@ -92,26 +93,29 @@ export interface CoreRow<TData extends RowData> {
9293
subRows: Row<TData>[]
9394
}
9495

95-
export const createRow = <TData extends RowData>(
96-
table: Table<TData>,
97-
id: string,
98-
original: TData,
99-
rowIndex: number,
100-
depth: number,
101-
subRows?: Row<TData>[],
102-
parentId?: string
103-
): Row<TData> => {
104-
let row: CoreRow<TData> = {
105-
id,
106-
index: rowIndex,
107-
original,
108-
depth,
109-
parentId,
110-
_valuesCache: {},
111-
_uniqueValuesCache: {},
112-
getValue: columnId => {
113-
if (row._valuesCache.hasOwnProperty(columnId)) {
114-
return row._valuesCache[columnId]
96+
const rowProtosByTable = new WeakMap<Table<any>, any>()
97+
98+
/**
99+
* Creates a table-specific row prototype object to hold shared row methods, including from all the
100+
* features that have been registered on the table.
101+
*/
102+
export function getRowProto<TData extends RowData>(table: Table<TData>) {
103+
let rowProto = rowProtosByTable.get(table)
104+
105+
if (!rowProto) {
106+
const proto = {} as CoreRow<TData>
107+
108+
proto.clone = function () {
109+
return Object.assign(Object.create(Object.getPrototypeOf(this)), this)
110+
}
111+
112+
// Make the default fallback value available on the proto itself to avoid duplicating it on every row instance
113+
// even if it's not used. This is safe as long as we don't mutate the value directly.
114+
proto.subRows = [] as const
115+
116+
proto.getValue = function (columnId: string) {
117+
if (this._valuesCache.hasOwnProperty(columnId)) {
118+
return this._valuesCache[columnId]
115119
}
116120

117121
const column = table.getColumn(columnId)
@@ -120,16 +124,22 @@ export const createRow = <TData extends RowData>(
120124
return undefined
121125
}
122126

123-
row._valuesCache[columnId] = column.accessorFn(
124-
row.original as TData,
125-
rowIndex
127+
this._valuesCache[columnId] = column.accessorFn(
128+
this.original as TData,
129+
this.index
126130
)
127131

128-
return row._valuesCache[columnId] as any
129-
},
130-
getUniqueValues: columnId => {
131-
if (row._uniqueValuesCache.hasOwnProperty(columnId)) {
132-
return row._uniqueValuesCache[columnId]
132+
return this._valuesCache[columnId] as any
133+
}
134+
135+
proto.getUniqueValues = function (columnId: string) {
136+
if (!this.hasOwnProperty('_uniqueValuesCache')) {
137+
// lazy-init cache on the instance
138+
this._uniqueValuesCache = {}
139+
}
140+
141+
if (this._uniqueValuesCache.hasOwnProperty(columnId)) {
142+
return this._uniqueValuesCache[columnId]
133143
}
134144

135145
const column = table.getColumn(columnId)
@@ -139,46 +149,58 @@ export const createRow = <TData extends RowData>(
139149
}
140150

141151
if (!column.columnDef.getUniqueValues) {
142-
row._uniqueValuesCache[columnId] = [row.getValue(columnId)]
143-
return row._uniqueValuesCache[columnId]
152+
this._uniqueValuesCache[columnId] = [this.getValue(columnId)]
153+
return this._uniqueValuesCache[columnId]
144154
}
145155

146-
row._uniqueValuesCache[columnId] = column.columnDef.getUniqueValues(
147-
row.original as TData,
148-
rowIndex
156+
this._uniqueValuesCache[columnId] = column.columnDef.getUniqueValues(
157+
this.original as TData,
158+
this.index
149159
)
150160

151-
return row._uniqueValuesCache[columnId] as any
152-
},
153-
renderValue: columnId =>
154-
row.getValue(columnId) ?? table.options.renderFallbackValue,
155-
subRows: subRows ?? [],
156-
getLeafRows: () => flattenBy(row.subRows, d => d.subRows),
157-
getParentRow: () =>
158-
row.parentId ? table.getRow(row.parentId, true) : undefined,
159-
getParentRows: () => {
161+
return this._uniqueValuesCache[columnId] as any
162+
}
163+
164+
proto.renderValue = function (columnId: string) {
165+
return this.getValue(columnId) ?? table.options.renderFallbackValue
166+
}
167+
168+
proto.getLeafRows = function () {
169+
return flattenBy(this.subRows, d => d.subRows)
170+
}
171+
172+
proto.getParentRow = function () {
173+
return this.parentId ? table.getRow(this.parentId, true) : undefined
174+
}
175+
176+
proto.getParentRows = function () {
160177
let parentRows: Row<TData>[] = []
161-
let currentRow = row
178+
let currentRow = this
162179
while (true) {
163180
const parentRow = currentRow.getParentRow()
164181
if (!parentRow) break
165182
parentRows.push(parentRow)
166183
currentRow = parentRow
167184
}
168185
return parentRows.reverse()
169-
},
170-
getAllCells: memo(
171-
() => [table.getAllLeafColumns()],
172-
leafColumns => {
186+
}
187+
188+
proto.getAllCells = memo(
189+
function (this: Row<TData>) {
190+
return [this, table.getAllLeafColumns()]
191+
},
192+
(row, leafColumns) => {
173193
return leafColumns.map(column => {
174-
return createCell(table, row as Row<TData>, column, column.id)
194+
return createCell(table, row, column, column.id)
175195
})
176196
},
177197
getMemoOptions(table.options, 'debugRows', 'getAllCells')
178-
),
198+
)
179199

180-
_getAllCellsByColumnId: memo(
181-
() => [row.getAllCells()],
200+
proto._getAllCellsByColumnId = memo(
201+
function (this: Row<TData>) {
202+
return [this.getAllCells()]
203+
},
182204
allCells => {
183205
return allCells.reduce(
184206
(acc, cell) => {
@@ -189,7 +211,36 @@ export const createRow = <TData extends RowData>(
189211
)
190212
},
191213
getMemoOptions(table.options, 'debugRows', 'getAllCellsByColumnId')
192-
),
214+
)
215+
216+
rowProtosByTable.set(table, proto)
217+
rowProto = proto
218+
}
219+
220+
return rowProto as CoreRow<TData>
221+
}
222+
223+
export const createRow = <TData extends RowData>(
224+
table: Table<TData>,
225+
id: string,
226+
original: TData,
227+
rowIndex: number,
228+
depth: number,
229+
subRows?: Row<TData>[],
230+
parentId?: string
231+
): Row<TData> => {
232+
const row: CoreRow<TData> = Object.create(getRowProto(table))
233+
Object.assign(row, {
234+
id,
235+
index: rowIndex,
236+
original,
237+
depth,
238+
parentId,
239+
_valuesCache: {},
240+
})
241+
242+
if (subRows) {
243+
row.subRows = subRows
193244
}
194245

195246
for (let i = 0; i < table._features.length; i++) {

packages/table-core/src/features/ColumnFiltering.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RowModel } from '..'
1+
import { getRowProto, RowModel } from '..'
22
import { BuiltInFilterFn, filterFns } from '../filterFns'
33
import {
44
Column,
@@ -362,14 +362,6 @@ export const ColumnFiltering: TableFeature = {
362362
}
363363
},
364364

365-
createRow: <TData extends RowData>(
366-
row: Row<TData>,
367-
_table: Table<TData>
368-
): void => {
369-
row.columnFilters = {}
370-
row.columnFiltersMeta = {}
371-
},
372-
373365
createTable: <TData extends RowData>(table: Table<TData>): void => {
374366
table.setColumnFilters = (updater: Updater<ColumnFiltersState>) => {
375367
const leafColumns = table.getAllLeafColumns()
@@ -411,6 +403,28 @@ export const ColumnFiltering: TableFeature = {
411403

412404
return table._getFilteredRowModel()
413405
}
406+
407+
// Lazy-init the backing caches on the instance so we don't take up memory for rows that don't need it
408+
Object.defineProperties(getRowProto(table), {
409+
columnFilters: {
410+
get() {
411+
return (this._columnFilters ??= {})
412+
},
413+
set(value) {
414+
this._columnFilters = value
415+
},
416+
enumerable: true,
417+
},
418+
columnFiltersMeta: {
419+
get() {
420+
return (this._columnFiltersMeta ??= {})
421+
},
422+
set(value) {
423+
this._columnFiltersMeta = value
424+
},
425+
enumerable: true,
426+
},
427+
})
414428
},
415429
}
416430

packages/table-core/src/features/ColumnGrouping.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RowModel } from '..'
1+
import { getRowProto, RowModel } from '..'
22
import { BuiltInAggregationFn, aggregationFns } from '../aggregationFns'
33
import {
44
AggregationFns,
@@ -353,31 +353,37 @@ export const ColumnGrouping: TableFeature = {
353353

354354
return table._getGroupedRowModel()
355355
}
356-
},
357356

358-
createRow: <TData extends RowData>(
359-
row: Row<TData>,
360-
table: Table<TData>
361-
): void => {
362-
row.getIsGrouped = () => !!row.groupingColumnId
363-
row.getGroupingValue = columnId => {
364-
if (row._groupingValuesCache.hasOwnProperty(columnId)) {
365-
return row._groupingValuesCache[columnId]
366-
}
357+
Object.defineProperty(getRowProto(table), '_groupingValuesCache', {
358+
get() {
359+
// Lazy-init the backing cache on the instance so we don't take up memory for rows that don't need it
360+
return (this.__groupingValuesCache ??= {})
361+
},
362+
enumerable: true,
363+
})
364+
365+
Object.assign(getRowProto(table), {
366+
getIsGrouped() {
367+
return !!this.groupingColumnId
368+
},
369+
getGroupingValue(columnId) {
370+
if (this._groupingValuesCache.hasOwnProperty(columnId)) {
371+
return this._groupingValuesCache[columnId]
372+
}
367373

368-
const column = table.getColumn(columnId)
374+
const column = table.getColumn(columnId)
369375

370-
if (!column?.columnDef.getGroupingValue) {
371-
return row.getValue(columnId)
372-
}
376+
if (!column?.columnDef.getGroupingValue) {
377+
return this.getValue(columnId)
378+
}
373379

374-
row._groupingValuesCache[columnId] = column.columnDef.getGroupingValue(
375-
row.original
376-
)
380+
this._groupingValuesCache[columnId] = column.columnDef.getGroupingValue(
381+
this.original
382+
)
377383

378-
return row._groupingValuesCache[columnId]
379-
}
380-
row._groupingValuesCache = {}
384+
return this._groupingValuesCache[columnId]
385+
},
386+
} as GroupingRow & Row<any>)
381387
},
382388

383389
createCell: <TData extends RowData, TValue>(

0 commit comments

Comments
 (0)