From 95736ad24ab0897e88e6c42bba8a098420807464 Mon Sep 17 00:00:00 2001 From: Matheus Wichman Date: Tue, 7 Feb 2023 10:16:24 -0300 Subject: [PATCH] [DataGrid] Make possible to memoize rows and cells --- .../components/DataGridProVirtualScroller.tsx | 17 +++-- .../x-data-grid/src/components/GridRow.tsx | 67 ++++++++++--------- .../virtualization/useGridVirtualScroller.tsx | 67 ++++++++++++++++--- 3 files changed, 100 insertions(+), 51 deletions(-) diff --git a/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx b/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx index b55ddf6283a26..18959da5b8666 100644 --- a/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx +++ b/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx @@ -233,13 +233,16 @@ const DataGridProVirtualScroller = React.forwardRef< [], ); - const getRowProps = (id: GridRowId) => { - if (!expandedRowIds.includes(id)) { - return null; - } - const height = detailPanelsHeights[id]; - return { style: { marginBottom: height } }; - }; + const getRowProps = React.useCallback( + (id: GridRowId) => { + if (!expandedRowIds.includes(id)) { + return null; + } + const height = detailPanelsHeights[id]; + return { style: { marginBottom: height } }; + }, + [detailPanelsHeights, expandedRowIds], + ); const pinnedColumns = useGridSelector(apiRef, gridPinnedColumnsSelector); const [leftPinnedColumns, rightPinnedColumns] = filterColumns( diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index 314f0f79201ed..baa7600153419 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -7,18 +7,12 @@ import { } from '@mui/utils'; import { GridRowEventLookup } from '../models/events'; import { GridRowId, GridRowModel, GridTreeNodeWithRender } from '../models/gridRows'; -import { - GridEditModes, - GridRowModes, - GridEditingState, - GridCellModes, -} from '../models/gridEditRowModel'; +import { GridEditModes, GridRowModes, GridCellModes } from '../models/gridEditRowModel'; import { useGridApiContext } from '../hooks/utils/useGridApiContext'; import { getDataGridUtilityClass, gridClasses } from '../constants/gridClasses'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import { DataGridProcessedProps } from '../models/props/DataGridProps'; import { GridStateColDef } from '../models/colDef/gridColDef'; -import { GridCellCoordinates } from '../models/gridCell'; import { gridColumnsTotalWidthSelector } from '../hooks/features/columns/gridColumnsSelector'; import { useGridSelector } from '../hooks/utils/useGridSelector'; import { GridRowClassNameParams } from '../models/params/gridRowParams'; @@ -33,6 +27,7 @@ import { gridRowMaximumTreeDepthSelector } from '../hooks/features/rows/gridRows import { gridColumnGroupsHeaderMaxDepthSelector } from '../hooks/features/columnGrouping/gridColumnGroupsSelector'; import { randomNumberBetween } from '../utils/utils'; import { GridCellProps } from './cell/GridCell'; +import { gridEditRowsStateSelector } from '../hooks/features/editing/gridEditingSelectors'; export interface GridRowProps { rowId: GridRowId; @@ -48,10 +43,17 @@ export interface GridRowProps { lastColumnToRender: number; visibleColumns: GridStateColDef[]; renderedColumns: GridStateColDef[]; - cellFocus: GridCellCoordinates | null; - cellTabIndex: GridCellCoordinates | null; - editRowsState: GridEditingState; position: 'left' | 'center' | 'right'; + /** + * Determines which cell has focus. + * If `null`, no cell in this row has focus. + */ + focusedCell: string | null; + /** + * Determines which cell should be tabbable by having tabIndex=0. + * If `null`, no cell in this row is in the tab sequence. + */ + tabbableCell: string | null; row?: GridRowModel; isLastVisible?: boolean; onClick?: React.MouseEventHandler; @@ -112,10 +114,9 @@ const GridRow = React.forwardRef< containerWidth, firstColumnToRender, lastColumnToRender, - cellFocus, - cellTabIndex, - editRowsState, isLastVisible = false, + focusedCell, + tabbableCell, onClick, onDoubleClick, onMouseEnter, @@ -130,6 +131,7 @@ const GridRow = React.forwardRef< const sortModel = useGridSelector(apiRef, gridSortModelSelector); const treeDepth = useGridSelector(apiRef, gridRowMaximumTreeDepthSelector); const headerGroupingMaxDepth = useGridSelector(apiRef, gridColumnGroupsHeaderMaxDepthSelector); + const editRowsState = useGridSelector(apiRef, gridEditRowsStateSelector); const handleRef = useForkRef(ref, refProp); const ariaRowIndex = index + headerGroupingMaxDepth + 2; // 1 for the header row and 1 as it's 1-based @@ -328,16 +330,8 @@ const GridRow = React.forwardRef< classNames.push(rootProps.getCellClassName(cellParams)); } - const hasFocus = - cellFocus !== null && cellFocus.id === rowId && cellFocus.field === column.field; - - const tabIndex = - cellTabIndex !== null && - cellTabIndex.id === rowId && - cellTabIndex.field === column.field && - cellParams.cellMode === 'view' - ? 0 - : -1; + const hasFocus = focusedCell === column.field; + const tabIndex = tabbableCell === column.field ? 0 : -1; const isSelected = apiRef.current.unstable_applyPipeProcessors('isCellSelected', false, { id: rowId, @@ -372,15 +366,15 @@ const GridRow = React.forwardRef< }, [ apiRef, - cellTabIndex, - editRowsState, - cellFocus, - rootProps, - row, - rowHeight, rowId, - treeDepth, + rootProps, sortModel.length, + treeDepth, + editRowsState, + focusedCell, + tabbableCell, + rowHeight, + row, ], ); @@ -517,11 +511,13 @@ GridRow.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- - cellFocus: PropTypes.object, - cellTabIndex: PropTypes.object, containerWidth: PropTypes.number.isRequired, - editRowsState: PropTypes.object.isRequired, firstColumnToRender: PropTypes.number.isRequired, + /** + * Determines which cell has focus. + * If `null`, no cell in this row has focus. + */ + focusedCell: PropTypes.string, /** * Index of the row in the whole sorted and filtered dataset. * If some rows above have expanded children, this index also take those children into account. @@ -535,6 +531,11 @@ GridRow.propTypes = { rowHeight: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired, rowId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, selected: PropTypes.bool.isRequired, + /** + * Determines which cell should be tabbable by having tabIndex=0. + * If `null`, no cell in this row is in the tab sequence. + */ + tabbableCell: PropTypes.string, visibleColumns: PropTypes.arrayOf(PropTypes.object).isRequired, } as any; diff --git a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index e9d189d169a44..d862300993953 100644 --- a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -14,17 +14,19 @@ import { gridColumnPositionsSelector, } from '../columns/gridColumnsSelector'; import { gridFocusCellSelector, gridTabIndexCellSelector } from '../focus/gridFocusStateSelector'; -import { gridEditRowsStateSelector } from '../editing/gridEditingSelectors'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { GridEventListener } from '../../../models/events'; +import { GridSlotsComponentsProps } from '../../../models/gridSlotsComponentsProps'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { clamp } from '../../../utils/utils'; import { GridRenderContext, GridRowEntry } from '../../../models'; import { selectedIdsLookupSelector } from '../rowSelection/gridRowSelectionSelector'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; import { GridRowId, GridRowModel } from '../../../models/gridRows'; +import { GridStateColDef } from '../../../models/colDef/gridColDef'; import { getFirstNonSpannedColumnToRender } from '../columns/gridColumnsUtils'; import { getMinimalContentHeight } from '../rows/gridRowsUtils'; +import { GridRowProps } from '../../../components/GridRow'; // Uses binary search to avoid looping through all possible positions export function binarySearch( @@ -112,7 +114,6 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { const cellFocus = useGridSelector(apiRef, gridFocusCellSelector); const cellTabIndex = useGridSelector(apiRef, gridTabIndexCellSelector); const rowsMeta = useGridSelector(apiRef, gridRowsMetaSelector); - const editRowsState = useGridSelector(apiRef, gridEditRowsStateSelector); const selectedRowsLookup = useGridSelector(apiRef, selectedIdsLookupSelector); const currentPage = useGridVisibleRows(apiRef, rootProps); const renderZoneRef = React.useRef(null); @@ -127,6 +128,15 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { }); const prevTotalWidth = React.useRef(columnsTotalWidth); + const rowStyleCache = React.useRef>({}); + const prevGetRowProps = React.useRef(); + const prevRootRowStyle = React.useRef(); + + const cachedRenderedColumns = React.useRef(); + const prevFirstColumnToRender = React.useRef(); + const prevLastColumnToRender = React.useRef(); + const prevVisibleColumns = React.useRef(); + const getNearestIndexToRender = React.useCallback( (offset: number) => { const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex(); @@ -488,7 +498,28 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { visibleRows: currentPage.rows, }); - const renderedColumns = visibleColumns.slice(firstColumnToRender, lastColumnToRender); + const invalidatesCachedRenderedColumns = + firstColumnToRender !== prevFirstColumnToRender.current || + lastColumnToRender !== prevLastColumnToRender.current || + visibleColumns !== prevVisibleColumns.current; + + if (invalidatesCachedRenderedColumns) { + cachedRenderedColumns.current = visibleColumns.slice(firstColumnToRender, lastColumnToRender); + prevFirstColumnToRender.current = firstColumnToRender; + prevLastColumnToRender.current = lastColumnToRender; + prevVisibleColumns.current = visibleColumns; + } + + const renderedColumns = cachedRenderedColumns.current; + + const { style: rootRowStyle, ...rootRowProps } = rootProps.componentsProps?.row || {}; + + const invalidatesCachedRowStyle = + prevGetRowProps.current !== getRowProps || prevRootRowStyle.current !== rootRowStyle; + + if (invalidatesCachedRowStyle) { + rowStyleCache.current = {}; + } const rows: JSX.Element[] = []; @@ -506,19 +537,33 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { isSelected = apiRef.current.isRowSelectable(id); } - const { style: rootRowStyle, ...rootRowProps } = rootProps.componentsProps?.row || {}; + const focusedCell = cellFocus !== null && cellFocus.id === id ? cellFocus.field : null; + + let tabbableCell: GridRowProps['tabbableCell'] = null; + if (cellTabIndex !== null && cellTabIndex.id === id) { + const cellParams = apiRef.current.getCellParams(id, cellTabIndex.field); + tabbableCell = cellParams.cellMode === 'view' ? cellTabIndex.field : null; + } + const { style: rowStyle, ...rowProps } = (typeof getRowProps === 'function' && getRowProps(id, model)) || {}; + if (!rowStyleCache.current[id]) { + const style = { + ...rowStyle, + ...rootRowStyle, + }; + rowStyleCache.current[id] = style; + } + rows.push( { position={position} {...rowProps} {...rootRowProps} - style={{ - ...rowStyle, - ...rootRowStyle, - }} + style={rowStyleCache.current[id]} />, ); } + prevGetRowProps.current = getRowProps; + prevRootRowStyle.current = rootRowStyle; + return rows; };