diff --git a/src/Body/index.tsx b/src/Body/index.tsx index 9c7431989..fed61a97b 100644 --- a/src/Body/index.tsx +++ b/src/Body/index.tsx @@ -8,6 +8,7 @@ import ResizeContext from '../context/ResizeContext'; import BodyRow from './BodyRow'; import useFlattenRecords from '../hooks/useFlattenRecords'; import HoverContext from '../context/HoverContext'; +import PerfContext, { PerfRecord } from '../context/PerfContext'; import MeasureRow from './MeasureRow'; export interface BodyProps { @@ -38,6 +39,11 @@ function Body({ const flattenData: { record: RecordType; indent: number; index: number }[] = useFlattenRecords(data, childrenColumnName, expandedKeys, getRowKey); + // =================== Performance ==================== + const perfRef = React.useRef({ + renderWithProps: false, + }); + // ====================== Hover ======================= const [startRow, setStartRow] = React.useState(-1); const [endRow, setEndRow] = React.useState(-1); @@ -132,7 +138,11 @@ function Body({ flattenData, ]); - return {bodyNode}; + return ( + + {bodyNode} + + ); } const MemoBody = React.memo(Body); diff --git a/src/Cell/index.tsx b/src/Cell/index.tsx index a1a83286e..a02a44c00 100644 --- a/src/Cell/index.tsx +++ b/src/Cell/index.tsx @@ -17,6 +17,7 @@ import StickyContext from '../context/StickyContext'; import HoverContext from '../context/HoverContext'; import type { HoverContextProps } from '../context/HoverContext'; import warning from 'rc-util/lib/warning'; +import PerfContext from '../context/PerfContext'; /** Check if cell is in hover range */ function inHoverRange(cellStartRow: number, cellRowSpan: number, startRow: number, endRow: number) { @@ -120,19 +121,23 @@ function Cell( ): React.ReactElement { const cellPrefixCls = `${prefixCls}-cell`; + const perfRecord = React.useContext(PerfContext); const supportSticky = React.useContext(StickyContext); // ==================== Child Node ==================== - let cellProps: CellType; - let childNode: React.ReactNode; + const [childNode, legacyCellProps] = React.useMemo< + [React.ReactNode, CellType] | [React.ReactNode] + >(() => { + if (validateValue(children)) { + return [children]; + } - if (validateValue(children)) { - childNode = children; - } else { const value = getPathValue(record, dataIndex); // Customize render node - childNode = value; + let returnChildNode = value; + let returnCellProps: CellType | undefined = undefined; + if (render) { const renderData = render(value, record, renderIndex); @@ -143,25 +148,41 @@ function Cell( '`columns.render` return cell props is deprecated with perf issue, please use `onCell` instead.', ); } - childNode = renderData.children; - cellProps = renderData.props; + returnChildNode = renderData.children; + returnCellProps = renderData.props; + perfRecord.renderWithProps = true; } else { - childNode = renderData; + returnChildNode = renderData; } } - } + + return [returnChildNode, returnCellProps]; + }, [ + /* eslint-disable react-hooks/exhaustive-deps */ + // Always re-render if `renderWithProps` + perfRecord.renderWithProps ? Math.random() : 0, + /* eslint-enable */ + children, + dataIndex, + perfRecord, + record, + render, + renderIndex, + ]); + + let mergedChildNode = childNode; // Not crash if final `childNode` is not validate ReactNode if ( - typeof childNode === 'object' && - !Array.isArray(childNode) && - !React.isValidElement(childNode) + typeof mergedChildNode === 'object' && + !Array.isArray(mergedChildNode) && + !React.isValidElement(mergedChildNode) ) { - childNode = null; + mergedChildNode = null; } if (ellipsis && (lastFixLeft || firstFixRight)) { - childNode = {childNode}; + mergedChildNode = {mergedChildNode}; } const { @@ -170,7 +191,7 @@ function Cell( style: cellStyle, className: cellClassName, ...restCellProps - } = cellProps || {}; + } = legacyCellProps || {}; const mergedColSpan = (cellColSpan !== undefined ? cellColSpan : colSpan) ?? 1; const mergedRowSpan = (cellRowSpan !== undefined ? cellRowSpan : rowSpan) ?? 1; @@ -220,10 +241,13 @@ function Cell( let title: string; const ellipsisConfig: CellEllipsisType = ellipsis === true ? { showTitle: true } : ellipsis; if (ellipsisConfig && (ellipsisConfig.showTitle || rowType === 'header')) { - if (typeof childNode === 'string' || typeof childNode === 'number') { - title = childNode.toString(); - } else if (React.isValidElement(childNode) && typeof childNode.props.children === 'string') { - title = childNode.props.children; + if (typeof mergedChildNode === 'string' || typeof mergedChildNode === 'number') { + title = mergedChildNode.toString(); + } else if ( + React.isValidElement(mergedChildNode) && + typeof mergedChildNode.props.children === 'string' + ) { + title = mergedChildNode.props.children; } } @@ -248,7 +272,7 @@ function Cell( [`${cellPrefixCls}-ellipsis`]: ellipsis, [`${cellPrefixCls}-with-append`]: appendNode, [`${cellPrefixCls}-fix-sticky`]: (isFixLeft || isFixRight) && isSticky && supportSticky, - [`${cellPrefixCls}-row-hover`]: !cellProps && hovering, + [`${cellPrefixCls}-row-hover`]: !legacyCellProps && hovering, }, additionalProps.className, cellClassName, @@ -262,7 +286,7 @@ function Cell( return ( {appendNode} - {childNode} + {mergedChildNode} ); } diff --git a/src/context/PerfContext.tsx b/src/context/PerfContext.tsx new file mode 100644 index 000000000..dcc41f49f --- /dev/null +++ b/src/context/PerfContext.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export interface PerfRecord { + renderWithProps: boolean; +} + +const PerfContext = React.createContext({ + renderWithProps: false, +}); + +export default PerfContext; diff --git a/tests/Hover.spec.tsx b/tests/Hover.spec.tsx index 2873713e0..651c9f91c 100644 --- a/tests/Hover.spec.tsx +++ b/tests/Hover.spec.tsx @@ -124,4 +124,82 @@ describe('Table.Hover', () => { wrapper.find('tbody td').at(1).simulate('mouseLeave'); expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); }); + + describe('perf', () => { + it('legacy mode should render every time', () => { + let renderTimes = 0; + + const wrapper = mount( + createTable({ + columns: [ + { + render: () => { + renderTimes += 1; + return { + children: null, + }; + }, + }, + ], + }), + ); + + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); + + // Hover 0-0 + renderTimes = 0; + wrapper.find('tbody td').at(0).simulate('mouseEnter'); + expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1); + expect(renderTimes).toBe(1); + + // Hover 0-1 + renderTimes = 0; + wrapper.find('tbody td').at(1).simulate('mouseEnter'); + expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1); + expect(renderTimes).toBe(2); + + // Mouse leave + renderTimes = 0; + wrapper.find('tbody td').at(1).simulate('mouseLeave'); + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); + expect(renderTimes).toBe(1); + }); + + it('perf mode to save render times', () => { + let renderTimes = 0; + + const wrapper = mount( + createTable({ + columns: [ + { + render: () => { + renderTimes += 1; + return null; + }, + }, + ], + }), + ); + + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); + + // Hover 0-0 + renderTimes = 0; + wrapper.find('tbody td').at(0).simulate('mouseEnter'); + expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1); + expect(renderTimes).toBe(0); + + // Hover 0-1 + renderTimes = 0; + wrapper.find('tbody td').at(1).simulate('mouseEnter'); + expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1); + expect(renderTimes).toBe(0); + + // Mouse leave + renderTimes = 0; + wrapper.find('tbody td').at(1).simulate('mouseLeave'); + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); + expect(renderTimes).toBe(0); + }); + }); });