Skip to content

Commit

Permalink
[DataGrid] Make possible to memoize rows and cells
Browse files Browse the repository at this point in the history
  • Loading branch information
m4theushw committed Feb 7, 2023
1 parent 729047d commit 95736ad
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
67 changes: 34 additions & 33 deletions packages/grid/x-data-grid/src/components/GridRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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<HTMLDivElement>;
Expand Down Expand Up @@ -112,10 +114,9 @@ const GridRow = React.forwardRef<
containerWidth,
firstColumnToRender,
lastColumnToRender,
cellFocus,
cellTabIndex,
editRowsState,
isLastVisible = false,
focusedCell,
tabbableCell,
onClick,
onDoubleClick,
onMouseEnter,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
],
);

Expand Down Expand Up @@ -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.
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<HTMLDivElement>(null);
Expand All @@ -127,6 +128,15 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => {
});
const prevTotalWidth = React.useRef(columnsTotalWidth);

const rowStyleCache = React.useRef<Record<GridRowId, any>>({});
const prevGetRowProps = React.useRef<UseGridVirtualScrollerProps['getRowProps']>();
const prevRootRowStyle = React.useRef<GridSlotsComponentsProps['row']>();

const cachedRenderedColumns = React.useRef<GridStateColDef[]>();
const prevFirstColumnToRender = React.useRef<number>();
const prevLastColumnToRender = React.useRef<number>();
const prevVisibleColumns = React.useRef<GridStateColDef[]>();

const getNearestIndexToRender = React.useCallback(
(offset: number) => {
const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
Expand Down Expand Up @@ -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[] = [];

Expand All @@ -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(
<rootProps.components.Row
key={id}
row={model}
rowId={id}
rowHeight={baseRowHeight}
cellFocus={cellFocus} // TODO move to inside the row
cellTabIndex={cellTabIndex} // TODO move to inside the row
editRowsState={editRowsState} // TODO move to inside the row
focusedCell={focusedCell}
tabbableCell={tabbableCell}
renderedColumns={renderedColumns}
visibleColumns={visibleColumns}
firstColumnToRender={firstColumnToRender}
Expand All @@ -530,14 +575,14 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => {
position={position}
{...rowProps}
{...rootRowProps}
style={{
...rowStyle,
...rootRowStyle,
}}
style={rowStyleCache.current[id]}
/>,
);
}

prevGetRowProps.current = getRowProps;
prevRootRowStyle.current = rootRowStyle;

return rows;
};

Expand Down

0 comments on commit 95736ad

Please sign in to comment.