Skip to content

Commit ab5c593

Browse files
authored
fix: row selection method cloning bug (#6314)
1 parent e25a532 commit ab5c593

3 files changed

Lines changed: 75 additions & 11 deletions

File tree

packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ export function selectRowsFn<
649649
): Array<Row<TFeatures, TData>> => {
650650
const result: Array<Row<TFeatures, TData>> = []
651651
for (let i = 0; i < rows.length; i++) {
652-
let row = rows[i]!
652+
const row = rows[i]!
653653
const isSelected = isRowSelected(row)
654654

655655
if (isSelected) {
@@ -658,13 +658,18 @@ export function selectRowsFn<
658658
}
659659

660660
if (row.subRows.length) {
661-
row = {
662-
...row,
663-
subRows: recurseRows(row.subRows, depth + 1),
661+
// Always recurse — selected descendants of unselected parents must
662+
// still be collected into flatRows/rowsById.
663+
const newSubRows = recurseRows(row.subRows, depth + 1)
664+
665+
if (isSelected) {
666+
// Preserve prototype chain so methods like getValue() remain accessible
667+
const cloned = Object.create(Object.getPrototypeOf(row))
668+
Object.assign(cloned, row)
669+
cloned.subRows = newSubRows
670+
result.push(cloned)
664671
}
665-
}
666-
667-
if (isSelected) {
672+
} else if (isSelected) {
668673
result.push(row)
669674
}
670675
}

packages/table-core/tests/implementation/features/row-selection/rowSelectionFeature.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,65 @@ describe('rowSelectionFeature', () => {
9696
expect(result.rowsById).toHaveProperty('0.0')
9797
})
9898

99+
it('should collect selected descendants of unselected parents', () => {
100+
const data = generateTestData(3, 2)
101+
const columns = generateColumnDefs(data)
102+
103+
const table = constructTable({
104+
features,
105+
rowModels: {},
106+
enableRowSelection: true,
107+
renderFallbackValue: '',
108+
data,
109+
getSubRows: (originalRow: Person, _idx: number) => originalRow.subRows,
110+
initialState: {
111+
rowSelection: {
112+
'0.0': true, // child selected, parent '0' is not
113+
},
114+
},
115+
columns,
116+
})
117+
const rowModel = table.getCoreRowModel()
118+
119+
const result = RowSelectionUtils.selectRowsFn(rowModel)
120+
121+
expect(result.rows.length).toBe(0)
122+
expect(result.flatRows.length).toBe(1)
123+
expect(result.flatRows[0]?.id).toBe('0.0')
124+
expect(result.rowsById).toHaveProperty('0.0')
125+
})
126+
127+
it('should preserve row prototype methods on cloned parent rows', () => {
128+
const data = generateTestData(3, 2)
129+
const columns = generateColumnDefs(data)
130+
131+
const table = constructTable<typeof features, Person>({
132+
features,
133+
rowModels: {},
134+
enableRowSelection: true,
135+
renderFallbackValue: '',
136+
data,
137+
getSubRows: (originalRow: Person, _idx: number) => originalRow.subRows,
138+
initialState: {
139+
rowSelection: {
140+
'0': true,
141+
'0.0': true,
142+
},
143+
},
144+
columns,
145+
})
146+
const rowModel = table.getCoreRowModel()
147+
148+
const result = RowSelectionUtils.selectRowsFn(rowModel)
149+
150+
// Selected parents with subRows are cloned; the clone must keep the
151+
// shared row prototype so APIs like getValue() still work
152+
const clonedParent = result.rows[0]!
153+
expect(clonedParent).not.toBe(rowModel.rows[0])
154+
expect(typeof clonedParent.getValue).toBe('function')
155+
expect(clonedParent.getValue('id')).toBe(rowModel.rows[0]!.getValue('id'))
156+
})
157+
99158
it('should return an empty list if no rows are selected', () => {
100159
const data = generateTestData(5)
101160
const columns = generateColumnDefs(data)

perf.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ Typecheck verified clean after the sweep (`pnpm tsc --noEmit` passes).
9494
## Progress
9595

9696
- **Total findings:** 61
97-
- **Done `[x]`:** 19
97+
- **Done `[x]`:** 20
9898
- **Partial `[~]`:** 2
9999
- **Skipped `[-]`:** 5
100-
- **Not started `[ ]`:** 35
100+
- **Not started `[ ]`:** 34
101101

102102
_(Update these counters as you go.)_
103103

@@ -1734,8 +1734,8 @@ Replace `let isAll = …; if (cond) isAll = false; return isAll` with `return !p
17341734

17351735
## 48. `selectRowsFn` spreads row object even when subRows did not change — Score: 4
17361736

1737-
**Status:** `[ ]` not started
1738-
**Implementation note:** _(none)_
1737+
**Status:** `[x]` done
1738+
**Implementation note:** Implemented a **reframed fix** — the proposed `newSubRows !== row.subRows` check is dead on arrival: `recurseRows` allocates a fresh `result` array on every call and returns it unconditionally, so the reference is _always_ different and the check could never skip a clone. (The scale table's premise is also inverted: an unmatched subtree returns `[]`, never the original reference.) The real waste in the same lines: the clone ran for **every** row with subRows, but unselected rows are never pushed to `result` — so their clone was allocated and immediately discarded. Fix: recurse unconditionally (required — selected descendants of unselected parents must still be collected into `flatRows`/`rowsById`), then build the clone only when `isSelected`. Under sparse selection on hierarchical data this eliminates nearly all clones. **Bug fix included:** rows are `Object.create(rowPrototype)` instances, but the old clone was a plain spread `{ ...row, subRows }` — which drops the prototype, so cloned parent rows in the selected row models lost all their prototype APIs (`getValue()`, etc.). The clone now uses the #49 precedent: `Object.assign(Object.create(Object.getPrototypeOf(row)), row)` + `subRows` assignment. Added regression tests (selected child under unselected parent; prototype-method survival on cloned parents) in `tests/implementation/features/row-selection/rowSelectionFeature.test.ts` — the existing suite only covered selected-child-under-selected-parent and would not have caught a recursion-skipping mistake.
17391739

17401740
**Location:** `src/features/row-selection/rowSelectionFeature.utils.ts:618–658`
17411741
**Category:** `micro`

0 commit comments

Comments
 (0)