Skip to content

Commit 8e21db3

Browse files
authored
fix(ember-table): run flexRender cell/header functions once per render pass (TanStack#6452)
The FlexRender components computed `result` in a plain getter that the template reads twice (component-branch check, then content), re-invoking the user's cell/header/footer render function on every read. Cache the getter so the render function runs exactly once per render pass.
1 parent ba45ea1 commit 8e21db3

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

packages/ember-table/src/FlexRender.gts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Component from '@glimmer/component'
2+
import { cached } from '@glimmer/tracking'
23
import { FlexRenderComponentConfig } from './flex-render-helpers.ts'
34
import { flexRender } from '@tanstack/table-core/flex-render'
45
import type {
@@ -74,6 +75,7 @@ export class FlexRenderCell<
7475
TData extends RowData,
7576
TValue extends CellData = CellData,
7677
> extends Component<FlexRenderCellSignature<TFeatures, TData, TValue>> {
78+
@cached
7779
get result(): CellRenderResult<TFeatures, TData, TValue> {
7880
const cell = this.args.cell
7981
return flexRender(
@@ -142,6 +144,7 @@ export class FlexRenderHeader<
142144
TData extends RowData,
143145
TValue extends CellData = CellData,
144146
> extends Component<FlexRenderHeaderSignature<TFeatures, TData, TValue>> {
147+
@cached
145148
get result(): HeaderRenderResult<TFeatures, TData, TValue> {
146149
const header = this.args.header
147150
if (header.isPlaceholder) return null
@@ -213,6 +216,7 @@ export class FlexRenderFooter<
213216
TData extends RowData,
214217
TValue extends CellData = CellData,
215218
> extends Component<FlexRenderFooterSignature<TFeatures, TData, TValue>> {
219+
@cached
216220
get result(): HeaderRenderResult<TFeatures, TData, TValue> {
217221
const footer = this.args.footer
218222
if (footer.isPlaceholder) return null
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { module, test } from 'qunit'
2+
import { render } from '@ember/test-helpers'
3+
import { setupRenderingTest } from 'ember-qunit'
4+
import Component from '@glimmer/component'
5+
import { tracked } from '@glimmer/tracking'
6+
import {
7+
useTable,
8+
FlexRenderCell,
9+
FlexRenderHeader,
10+
stockFeatures,
11+
type Row,
12+
type Cell,
13+
type ColumnDef,
14+
} from '#src/index.ts'
15+
16+
type Person = { id: string; firstName: string }
17+
18+
const getVisibleCells = (
19+
row: Row<typeof stockFeatures, Person>,
20+
): Array<Cell<typeof stockFeatures, Person>> => row.getVisibleCells()
21+
22+
// The result of a columnDef `cell`/`header` render function is read by the
23+
// FlexRender template more than once (branch check + content). Those reads
24+
// must not re-invoke the user's render function: it should run exactly once
25+
// per cell per render pass.
26+
module('Integration | FlexRender | invocation count', function (hooks) {
27+
setupRenderingTest(hooks)
28+
29+
test('cell and header render functions run once per render pass', async (assert) => {
30+
const calls = { cell: 0, header: 0 }
31+
32+
const columns: ColumnDef<typeof stockFeatures, Person, unknown>[] = [
33+
{
34+
id: 'firstName',
35+
accessorFn: (row: Person) => row.firstName,
36+
header: () => {
37+
calls.header++
38+
return 'First name'
39+
},
40+
cell: (info) => {
41+
calls.cell++
42+
return info.getValue<string>()
43+
},
44+
},
45+
]
46+
47+
class TableComponent extends Component {
48+
@tracked data: Array<Person> = [
49+
{ id: '1', firstName: 'Alice' },
50+
{ id: '2', firstName: 'Bob' },
51+
]
52+
53+
table = useTable(() => ({
54+
data: this.data,
55+
features: stockFeatures,
56+
columns,
57+
getRowId: (row: Person) => row.id,
58+
}))
59+
60+
get rows() {
61+
return this.table.getRowModel().rows
62+
}
63+
64+
get headers() {
65+
return this.table.getHeaderGroups()
66+
}
67+
68+
<template>
69+
<table>
70+
<thead>
71+
{{#each this.headers as |headerGroup|}}
72+
<tr>
73+
{{#each headerGroup.headers as |header|}}
74+
<th><FlexRenderHeader @header={{header}} /></th>
75+
{{/each}}
76+
</tr>
77+
{{/each}}
78+
</thead>
79+
<tbody>
80+
{{#each this.rows key='id' as |row|}}
81+
<tr data-test-row={{row.id}}>
82+
{{#each (getVisibleCells row) key='id' as |cell|}}
83+
<td><FlexRenderCell @cell={{cell}} /></td>
84+
{{/each}}
85+
</tr>
86+
{{/each}}
87+
</tbody>
88+
</table>
89+
</template>
90+
}
91+
92+
await render(<template><TableComponent /></template>)
93+
94+
assert.dom('td').exists({ count: 2 })
95+
assert.dom('[data-test-row="1"] td').hasText('Alice')
96+
assert.strictEqual(
97+
calls.cell,
98+
2,
99+
'cell fn ran exactly once per cell on initial render',
100+
)
101+
assert.strictEqual(
102+
calls.header,
103+
1,
104+
'header fn ran exactly once on initial render',
105+
)
106+
})
107+
})

0 commit comments

Comments
 (0)