Skip to content

Commit c3558d8

Browse files
committed
fix: prevent duplicate force-rendered content in virtual tables
1 parent 536bdc8 commit c3558d8

6 files changed

Lines changed: 152 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Then open `http://localhost:8000`.
8282
| expandable.defaultExpandedRowKeys | String[] | [] | initial expanded rows keys |
8383
| expandable.expandedRowKeys | String[] | | current expanded rows keys |
8484
| expandable.expandedRowRender | Function(recode, index, indent, expanded):ReactNode | | Content render to expanded row |
85-
| expandable.forceRender | Boolean | false | Force render expanded row content before expansion |
85+
| expandable.forceRender | Boolean | false | Force render expanded row content before expansion. In virtual mode, only rows currently mounted by the virtual list are force-rendered; off-screen rows may still be unmounted |
8686
| expandable.expandedRowClassName | `string` \| `(recode, index, indent) => string` | | get expanded row's className |
8787
| expandable.expandRowByClick | boolean | | Support expand by click row |
8888
| expandable.expandIconColumnIndex | Number | 0 | The index of expandIcon which column will be inserted when expandIconAsCell is false |

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ npm start
8282
| expandable.defaultExpandedRowKeys | String[] | [] | 初始扩展行键 |
8383
| expandable.expandedRowKeys | String[] | | 当前扩展行键 |
8484
| expandable.expandedRowRender | Function(recode, index, indent, expanded):ReactNode | | 内容渲染到扩展行 |
85-
| expandable.forceRender | Boolean | false | 在展开前强制渲染展开行内容 |
85+
| expandable.forceRender | Boolean | false | 在展开前强制渲染展开行内容。虚拟模式下,仅强制渲染虚拟列表当前挂载的行;屏幕外的行仍可能被卸载 |
8686
| expandable.expandedRowClassName | `string` \| `(recode, index, indent) => string` | | 获取扩展行的 className |
8787
| expandable.expandRowByClick | boolean | | 支持点击行展开 |
8888
| expandable.expandIconColumnIndex | Number | 0 | ExpandIconAsCell 为 false 时将插入哪一列的 ExpandIcon 索引 |

src/Table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -910,7 +910,7 @@ const Table = <RecordType extends DefaultRecordType>(
910910
expandableType,
911911
expandRowByClick: expandableConfig.expandRowByClick,
912912
expandedRowRender: expandableConfig.expandedRowRender,
913-
forceRender: expandableConfig.forceRender,
913+
forceRender: expandableConfig.forceRender ?? false,
914914
expandedRowOffset: expandableConfig.expandedRowOffset,
915915
onTriggerExpand,
916916
expandIconColumnIndex: expandableConfig.expandIconColumnIndex,

src/VirtualTable/BodyLine.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const BodyLine = React.forwardRef<HTMLDivElement, BodyLineProps>((props, ref) =>
5555
const expandedClsName = computedExpandedClassName(expandedRowClassName, record, index, indent);
5656

5757
let expandRowNode: React.ReactElement<any>;
58-
if (rowSupportExpand && (forceRender || expanded)) {
58+
if (!extra && rowSupportExpand && (forceRender || expanded)) {
5959
const expandContent = expandedRowRender(record, index, indent + 1, expanded);
6060

6161
let additionalProps: React.TdHTMLAttributes<HTMLElement> = {};

tests/ExpandRow.spec.jsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,65 @@ describe('Table.Expand', () => {
7070
expect(expandedRows[1]).toHaveStyle({ display: 'none' });
7171
});
7272

73+
it('keeps force-rendered expanded content mounted across expand and collapse', () => {
74+
const onMount = vi.fn();
75+
const onUnmount = vi.fn();
76+
77+
class StatefulContent extends React.Component {
78+
state = { count: 0 };
79+
80+
componentDidMount() {
81+
onMount();
82+
}
83+
84+
componentWillUnmount() {
85+
onUnmount();
86+
}
87+
88+
render() {
89+
return (
90+
<button
91+
type="button"
92+
className="stateful-expanded-content"
93+
onClick={() => this.setState(({ count }) => ({ count: count + 1 }))}
94+
>
95+
{this.state.count}
96+
</button>
97+
);
98+
}
99+
}
100+
101+
const { container } = render(
102+
createTable({
103+
data: [sampleData[0]],
104+
expandable: {
105+
expandedRowRender: () => <StatefulContent />,
106+
forceRender: true,
107+
},
108+
}),
109+
);
110+
111+
const expandIcon = container.querySelector('.rc-table-row-expand-icon');
112+
const content = container.querySelector('.stateful-expanded-content');
113+
114+
expect(onMount).toHaveBeenCalledTimes(1);
115+
expect(onUnmount).not.toHaveBeenCalled();
116+
117+
fireEvent.click(expandIcon);
118+
fireEvent.click(content);
119+
expect(content).toHaveTextContent('1');
120+
121+
fireEvent.click(expandIcon);
122+
expect(container.querySelector('.stateful-expanded-content')).toBe(content);
123+
expect(onUnmount).not.toHaveBeenCalled();
124+
125+
fireEvent.click(expandIcon);
126+
expect(container.querySelector('.stateful-expanded-content')).toBe(content);
127+
expect(content).toHaveTextContent('1');
128+
expect(onMount).toHaveBeenCalledTimes(1);
129+
expect(onUnmount).not.toHaveBeenCalled();
130+
});
131+
73132
it('renders tree row correctly', () => {
74133
const data = [
75134
{

tests/Virtual.spec.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,95 @@ describe('Table.Virtual', () => {
211211
});
212212
});
213213

214+
it('keeps force-rendered expanded content mounted across expand and collapse', () => {
215+
const onMount = vi.fn();
216+
const onUnmount = vi.fn();
217+
218+
class StatefulContent extends React.Component<unknown, { count: number }> {
219+
state = { count: 0 };
220+
221+
componentDidMount() {
222+
onMount();
223+
}
224+
225+
componentWillUnmount() {
226+
onUnmount();
227+
}
228+
229+
render() {
230+
return (
231+
<button
232+
type="button"
233+
className="stateful-expanded-content"
234+
onClick={() => this.setState(({ count }) => ({ count: count + 1 }))}
235+
>
236+
{this.state.count}
237+
</button>
238+
);
239+
}
240+
}
241+
242+
const { container } = getTable({
243+
data: [{ name: 'name0', age: 0, address: 'address0' }],
244+
expandable: {
245+
expandedRowRender: () => <StatefulContent />,
246+
forceRender: true,
247+
},
248+
});
249+
250+
const expandIcon = container.querySelector('.rc-table-row-expand-icon')!;
251+
const content = container.querySelector('.stateful-expanded-content')!;
252+
253+
expect(onMount).toHaveBeenCalledTimes(1);
254+
expect(onUnmount).not.toHaveBeenCalled();
255+
256+
fireEvent.click(expandIcon);
257+
fireEvent.click(content);
258+
expect(content).toHaveTextContent('1');
259+
260+
fireEvent.click(expandIcon);
261+
expect(container.querySelector('.stateful-expanded-content')).toBe(content);
262+
expect(onUnmount).not.toHaveBeenCalled();
263+
264+
fireEvent.click(expandIcon);
265+
expect(container.querySelector('.stateful-expanded-content')).toBe(content);
266+
expect(content).toHaveTextContent('1');
267+
expect(onMount).toHaveBeenCalledTimes(1);
268+
expect(onUnmount).not.toHaveBeenCalled();
269+
});
270+
271+
it('does not duplicate force-rendered content for rowSpan overlay lines', () => {
272+
const expandedRowRender = vi.fn((record: { name: string }) => (
273+
<span data-expanded-record={record.name}>{record.name}</span>
274+
));
275+
const data = [
276+
{ name: 'name0', age: 0, address: 'address0' },
277+
{ name: 'name1', age: 1, address: 'address1' },
278+
];
279+
const { container } = getTable({
280+
data,
281+
columns: [
282+
{
283+
dataIndex: 'name',
284+
onCell: (_, index) => ({
285+
rowSpan: index === 0 ? 2 : 0,
286+
}),
287+
},
288+
],
289+
expandable: {
290+
expandedRowRender,
291+
forceRender: true,
292+
},
293+
});
294+
295+
expect(container.querySelector('.rc-table-row-extra')).toBeTruthy();
296+
expect(expandedRowRender).toHaveBeenCalledTimes(2);
297+
expect(expandedRowRender).toHaveBeenNthCalledWith(1, data[0], 0, 1, false);
298+
expect(expandedRowRender).toHaveBeenNthCalledWith(2, data[1], 1, 1, false);
299+
expect(container.querySelectorAll('[data-expanded-record="name0"]')).toHaveLength(1);
300+
expect(container.querySelectorAll('[data-expanded-record="name1"]')).toHaveLength(1);
301+
});
302+
214303
it('applies expanded row class to tree rows', () => {
215304
const { container } = getTable({
216305
data: [

0 commit comments

Comments
 (0)