Skip to content

Commit

Permalink
[data grid] Avoid subscribing to renderContext state in grid root f…
Browse files Browse the repository at this point in the history
…or better scroll performance (#15986)
  • Loading branch information
lauri865 authored Dec 26, 2024
1 parent 758d8b8 commit b061f55
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
GridStrategyGroup,
GridStrategyProcessor,
useGridRegisterStrategyProcessor,
runIf,
} from '@mui/x-data-grid/internals';
import { GridPrivateApiPro } from '../../../models/gridApiPro';
import { DataGridProProcessedProps } from '../../../models/dataGridProProps';
Expand All @@ -26,7 +27,6 @@ import {
DataSourceRowsUpdateStrategy,
NestedDataManager,
RequestStatus,
runIf,
} from './utils';
import { GridDataSourceCache } from '../../../models';
import { GridDataSourceCacheDefault, GridDataSourceCacheDefaultConfig } from './cache';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import {
GridStrategyGroup,
GridStrategyProcessor,
useGridRegisterStrategyProcessor,
runIf,
} from '@mui/x-data-grid/internals';
import { GridPrivateApiPro } from '../../../models/gridApiPro';
import { DataGridProProcessedProps } from '../../../models/dataGridProProps';
import { findSkeletonRowsSection } from '../lazyLoader/utils';
import { GRID_SKELETON_ROW_ROOT_ID } from '../lazyLoader/useGridLazyLoaderPreProcessors';
import { DataSourceRowsUpdateStrategy, runIf } from '../dataSource/utils';
import { DataSourceRowsUpdateStrategy } from '../dataSource/utils';

enum LoadingTrigger {
VIEWPORT,
Expand Down Expand Up @@ -72,7 +73,6 @@ export const useGridDataSourceLazyLoader = (
const paginationModel = useGridSelector(privateApiRef, gridPaginationModelSelector);
const filteredSortedRowIds = useGridSelector(privateApiRef, gridFilteredSortedRowIdsSelector);
const dimensions = useGridSelector(privateApiRef, gridDimensionsSelector);
const renderContext = useGridSelector(privateApiRef, gridRenderContextSelector);
const renderedRowsIntervalCache = React.useRef(INTERVAL_CACHE_INITIAL_STATE);
const previousLastRowIndex = React.useRef(0);
const loadingTrigger = React.useRef<LoadingTrigger | null>(null);
Expand Down Expand Up @@ -308,6 +308,7 @@ export const useGridDataSourceLazyLoader = (

const handleScrolling: GridEventListener<'scrollPositionChange'> = React.useCallback(
(newScrollPosition) => {
const renderContext = gridRenderContextSelector(privateApiRef);
if (
loadingTrigger.current !== LoadingTrigger.SCROLL_END ||
previousLastRowIndex.current >= renderContext.lastRowIndex
Expand Down Expand Up @@ -342,7 +343,6 @@ export const useGridDataSourceLazyLoader = (
filterModel,
dimensions,
paginationModel.pageSize,
renderContext.lastRowIndex,
adjustRowParams,
],
);
Expand Down Expand Up @@ -419,6 +419,7 @@ export const useGridDataSourceLazyLoader = (
(newSortModel) => {
rowsStale.current = true;
previousLastRowIndex.current = 0;
const renderContext = gridRenderContextSelector(privateApiRef);
const rangeParams =
loadingTrigger.current === LoadingTrigger.VIEWPORT
? {
Expand All @@ -442,7 +443,7 @@ export const useGridDataSourceLazyLoader = (
adjustRowParams(getRowsParams),
);
},
[privateApiRef, filterModel, paginationModel.pageSize, renderContext, adjustRowParams],
[privateApiRef, filterModel, paginationModel.pageSize, adjustRowParams],
);

const handleGridFilterModelChange = React.useCallback<GridEventListener<'filterModelChange'>>(
Expand Down
200 changes: 101 additions & 99 deletions packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,18 @@ import * as React from 'react';
import useLazyRef from '@mui/utils/useLazyRef';
import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../internals/constants';
import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSelector';
import { useGridVisibleRows } from '../../utils/useGridVisibleRows';
import { getVisibleRows } from '../../utils/useGridVisibleRows';
import { gridRenderContextSelector } from '../virtualization/gridVirtualizationSelectors';
import { useGridSelector } from '../../utils/useGridSelector';
import { gridRowTreeSelector } from './gridRowsSelector';
import { GridRenderContext } from '../../../models';
import type { GridColDef } from '../../../models/colDef';
import type { GridRowId, GridValidRowModel, GridRowEntry } from '../../../models/gridRows';
import type { DataGridProcessedProps } from '../../../models/props/DataGridProps';
import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity';
import type { GridStateInitializer } from '../../utils/useGridInitializeState';
import {
getUnprocessedRange,
isRowRangeUpdated,
isRowContextInitialized,
getCellValue,
} from './gridRowSpanningUtils';
import { getUnprocessedRange, isRowContextInitialized, getCellValue } from './gridRowSpanningUtils';
import { GRID_CHECKBOX_SELECTION_FIELD } from '../../../colDef/gridCheckboxSelectionColDef';
import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { runIf } from '../../../utils/utils';

export interface GridRowSpanningState {
spannedCells: Record<GridRowId, Record<GridColDef['field'], number>>;
Expand Down Expand Up @@ -94,9 +90,10 @@ const computeRowSpanningState = (
const backwardsHiddenCells: number[] = [];
if (index === rangeToProcess.firstRowIndex) {
let prevIndex = index - 1;
const prevRowEntry = visibleRows[prevIndex];
let prevRowEntry = visibleRows[prevIndex];
while (
prevIndex >= range.firstRowIndex &&
prevRowEntry &&
getCellValue(prevRowEntry.model, colDef, apiRef) === cellValue
) {
const currentRow = visibleRows[prevIndex + 1];
Expand All @@ -110,6 +107,8 @@ const computeRowSpanningState = (
spannedRowId = prevRowEntry.id;
spannedRowIndex = prevIndex;
prevIndex -= 1;

prevRowEntry = visibleRows[prevIndex];
}
}

Expand Down Expand Up @@ -165,69 +164,66 @@ const computeRowSpanningState = (
* @requires filterStateInitializer (method) - should be initialized before
*/
export const rowSpanningStateInitializer: GridStateInitializer = (state, props, apiRef) => {
if (props.rowSpanning) {
const rowIds = state.rows!.dataRowIds || [];
const orderedFields = state.columns!.orderedFields || [];
const dataRowIdToModelLookup = state.rows!.dataRowIdToModelLookup;
const columnsLookup = state.columns!.lookup;
const isFilteringPending =
Boolean(state.filter!.filterModel!.items!.length) ||
Boolean(state.filter!.filterModel!.quickFilterValues?.length);

if (
!rowIds.length ||
!orderedFields.length ||
!dataRowIdToModelLookup ||
!columnsLookup ||
isFilteringPending
) {
return {
...state,
rowSpanning: EMPTY_STATE,
};
}
const rangeToProcess = {
firstRowIndex: 0,
lastRowIndex: Math.min(DEFAULT_ROWS_TO_PROCESS, Math.max(rowIds.length, 0)),
if (!props.rowSpanning) {
return {
...state,
rowSpanning: EMPTY_STATE,
};
const rows = rowIds.map((id) => ({
id,
model: dataRowIdToModelLookup[id!],
})) as GridRowEntry<GridValidRowModel>[];
const colDefs = orderedFields.map((field) => columnsLookup[field!]) as GridColDef[];
const { spannedCells, hiddenCells, hiddenCellOriginMap } = computeRowSpanningState(
apiRef,
colDefs,
rows,
rangeToProcess,
rangeToProcess,
true,
EMPTY_RANGE,
);
}

const rowIds = state.rows!.dataRowIds || [];
const orderedFields = state.columns!.orderedFields || [];
const dataRowIdToModelLookup = state.rows!.dataRowIdToModelLookup;
const columnsLookup = state.columns!.lookup;
const isFilteringPending =
Boolean(state.filter!.filterModel!.items!.length) ||
Boolean(state.filter!.filterModel!.quickFilterValues?.length);

if (
!rowIds.length ||
!orderedFields.length ||
!dataRowIdToModelLookup ||
!columnsLookup ||
isFilteringPending
) {
return {
...state,
rowSpanning: {
spannedCells,
hiddenCells,
hiddenCellOriginMap,
},
rowSpanning: EMPTY_STATE,
};
}
const rangeToProcess = {
firstRowIndex: 0,
lastRowIndex: Math.min(DEFAULT_ROWS_TO_PROCESS, Math.max(rowIds.length, 0)),
};
const rows = rowIds.map((id) => ({
id,
model: dataRowIdToModelLookup[id!],
})) as GridRowEntry<GridValidRowModel>[];
const colDefs = orderedFields.map((field) => columnsLookup[field!]) as GridColDef[];
const { spannedCells, hiddenCells, hiddenCellOriginMap } = computeRowSpanningState(
apiRef,
colDefs,
rows,
rangeToProcess,
rangeToProcess,
true,
EMPTY_RANGE,
);

return {
...state,
rowSpanning: EMPTY_STATE,
rowSpanning: {
spannedCells,
hiddenCells,
hiddenCellOriginMap,
},
};
};

export const useGridRowSpanning = (
apiRef: React.MutableRefObject<GridPrivateApiCommunity>,
props: Pick<DataGridProcessedProps, 'rowSpanning' | 'pagination' | 'paginationMode'>,
): void => {
const { range, rows: visibleRows } = useGridVisibleRows(apiRef, props);
const renderContext = useGridSelector(apiRef, gridRenderContextSelector);
const colDefs = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector);
const tree = useGridSelector(apiRef, gridRowTreeSelector);
const processedRange = useLazyRef<RowRange, void>(() => {
return Object.keys(apiRef.current.state.rowSpanning.spannedCells).length > 0
? {
Expand All @@ -239,23 +235,13 @@ export const useGridRowSpanning = (
}
: EMPTY_RANGE;
});
const lastRange = React.useRef<RowRange>(EMPTY_RANGE);

const updateRowSpanningState = React.useCallback(
// A reset needs to occur when:
// - The `unstable_rowSpanning` prop is updated (feature flag)
// - The filtering is applied
// - The sorting is applied
// - The `paginationModel` is updated
// - The rows are updated
(resetState: boolean = true) => {
if (!props.rowSpanning) {
if (apiRef.current.state.rowSpanning !== EMPTY_STATE) {
apiRef.current.setState((state) => ({ ...state, rowSpanning: EMPTY_STATE }));
}
return;
}

(renderContext: GridRenderContext, resetState: boolean = false) => {
const { range, rows: visibleRows } = getVisibleRows(apiRef, {
pagination: props.pagination,
paginationMode: props.paginationMode,
});
if (range === null || !isRowContextInitialized(renderContext)) {
return;
}
Expand All @@ -276,6 +262,7 @@ export const useGridRowSpanning = (
return;
}

const colDefs = gridVisibleColumnDefinitionsSelector(apiRef);
const {
spannedCells,
hiddenCells,
Expand Down Expand Up @@ -306,8 +293,9 @@ export const useGridRowSpanning = (
resetState ||
newSpannedCellsCount !== currentSpannedCellsCount ||
newHiddenCellsCount !== currentHiddenCellsCount;
const hasNoSpannedCells = newSpannedCellsCount === 0 && currentSpannedCellsCount === 0;

if (!shouldUpdateState) {
if (!shouldUpdateState || hasNoSpannedCells) {
return;
}

Expand All @@ -322,35 +310,49 @@ export const useGridRowSpanning = (
};
});
},
[apiRef, props.rowSpanning, range, renderContext, visibleRows, colDefs, processedRange],
[apiRef, processedRange, props.pagination, props.paginationMode],
);

const prevRenderContext = React.useRef(renderContext);
const isFirstRender = React.useRef(true);
const shouldResetState = React.useRef(false);
const previousTree = React.useRef(tree);
React.useEffect(() => {
const firstRender = isFirstRender.current;
if (isFirstRender.current) {
isFirstRender.current = false;
}
if (tree !== previousTree.current) {
previousTree.current = tree;
updateRowSpanningState(true);
// Reset events trigger a full re-computation of the row spanning state:
// - The `unstable_rowSpanning` prop is updated (feature flag)
// - The filtering is applied
// - The sorting is applied
// - The `paginationModel` is updated
// - The rows are updated
const resetRowSpanningState = React.useCallback(() => {
const renderContext = gridRenderContextSelector(apiRef);
if (!isRowContextInitialized(renderContext)) {
return;
}
if (range && lastRange.current && isRowRangeUpdated(range, lastRange.current)) {
lastRange.current = range;
shouldResetState.current = true;
}
if (!firstRender && prevRenderContext.current !== renderContext) {
if (isRowRangeUpdated(prevRenderContext.current, renderContext)) {
updateRowSpanningState(shouldResetState.current);
shouldResetState.current = false;
updateRowSpanningState(renderContext, true);
}, [apiRef, updateRowSpanningState]);

useGridApiEventHandler(
apiRef,
'renderedRowsIntervalChange',
runIf(props.rowSpanning, updateRowSpanningState),
);

useGridApiEventHandler(apiRef, 'sortedRowsSet', runIf(props.rowSpanning, resetRowSpanningState));
useGridApiEventHandler(
apiRef,
'paginationModelChange',
runIf(props.rowSpanning, resetRowSpanningState),
);
useGridApiEventHandler(
apiRef,
'filteredRowsSet',
runIf(props.rowSpanning, resetRowSpanningState),
);
useGridApiEventHandler(apiRef, 'columnsChange', runIf(props.rowSpanning, resetRowSpanningState));

React.useEffect(() => {
if (!props.rowSpanning) {
if (apiRef.current.state.rowSpanning !== EMPTY_STATE) {
apiRef.current.setState((state) => ({ ...state, rowSpanning: EMPTY_STATE }));
}
prevRenderContext.current = renderContext;
return;
} else if (apiRef.current.state.rowSpanning === EMPTY_STATE) {
resetRowSpanningState();
}
updateRowSpanningState();
}, [updateRowSpanningState, renderContext, range, lastRange, tree]);
}, [apiRef, resetRowSpanningState, props.rowSpanning]);
};
7 changes: 5 additions & 2 deletions packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ describe('<DataGrid /> - Row spanning', () => {

describe('rows update', () => {
it('should update the row spanning state when the rows are updated', () => {
const rowSpanValueGetter = spy();
const rowSpanValueGetter = spy((value) => value);
let rowSpanningStateUpdates = 0;
let spannedCells = {};
render(
Expand All @@ -271,7 +271,10 @@ describe('<DataGrid /> - Row spanning', () => {
expect(rowSpanningStateUpdates).to.equal(1);

act(() => {
apiRef.current.setRows([{ id: 1, code: 'A101' }]);
apiRef.current.setRows([
{ id: 1, code: 'A101' },
{ id: 2, code: 'A101' },
]);
});

// Second update on row update
Expand Down
6 changes: 6 additions & 0 deletions packages/x-data-grid/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,9 @@ export function deepClone(obj: Record<string, any>) {
* that hint disables checks on all values instead of just one.
*/
export function eslintUseValue(_: any) {}

export const runIf = (condition: boolean, fn: Function) => (params: unknown) => {
if (condition) {
fn(params);
}
};

0 comments on commit b061f55

Please sign in to comment.