diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts index 243af1a9e34c..d13d64e1895d 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts @@ -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'; @@ -26,7 +27,6 @@ import { DataSourceRowsUpdateStrategy, NestedDataManager, RequestStatus, - runIf, } from './utils'; import { GridDataSourceCache } from '../../../models'; import { GridDataSourceCacheDefault, GridDataSourceCacheDefaultConfig } from './cache'; diff --git a/packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts b/packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts index 2b940e816308..c36a874f5ef8 100644 --- a/packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts +++ b/packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts @@ -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, @@ -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(null); @@ -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 @@ -342,7 +343,6 @@ export const useGridDataSourceLazyLoader = ( filterModel, dimensions, paginationModel.pageSize, - renderContext.lastRowIndex, adjustRowParams, ], ); @@ -419,6 +419,7 @@ export const useGridDataSourceLazyLoader = ( (newSortModel) => { rowsStale.current = true; previousLastRowIndex.current = 0; + const renderContext = gridRenderContextSelector(privateApiRef); const rangeParams = loadingTrigger.current === LoadingTrigger.VIEWPORT ? { @@ -442,7 +443,7 @@ export const useGridDataSourceLazyLoader = ( adjustRowParams(getRowsParams), ); }, - [privateApiRef, filterModel, paginationModel.pageSize, renderContext, adjustRowParams], + [privateApiRef, filterModel, paginationModel.pageSize, adjustRowParams], ); const handleGridFilterModelChange = React.useCallback>( diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index 8ee70db58ad2..b70494fd17c0 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -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>; @@ -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]; @@ -110,6 +107,8 @@ const computeRowSpanningState = ( spannedRowId = prevRowEntry.id; spannedRowIndex = prevIndex; prevIndex -= 1; + + prevRowEntry = visibleRows[prevIndex]; } } @@ -165,58 +164,59 @@ 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[]; - 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[]; + 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, + }, }; }; @@ -224,10 +224,6 @@ export const useGridRowSpanning = ( apiRef: React.MutableRefObject, props: Pick, ): 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(() => { return Object.keys(apiRef.current.state.rowSpanning.spannedCells).length > 0 ? { @@ -239,23 +235,13 @@ export const useGridRowSpanning = ( } : EMPTY_RANGE; }); - const lastRange = React.useRef(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; } @@ -276,6 +262,7 @@ export const useGridRowSpanning = ( return; } + const colDefs = gridVisibleColumnDefinitionsSelector(apiRef); const { spannedCells, hiddenCells, @@ -306,8 +293,9 @@ export const useGridRowSpanning = ( resetState || newSpannedCellsCount !== currentSpannedCellsCount || newHiddenCellsCount !== currentHiddenCellsCount; + const hasNoSpannedCells = newSpannedCellsCount === 0 && currentSpannedCellsCount === 0; - if (!shouldUpdateState) { + if (!shouldUpdateState || hasNoSpannedCells) { return; } @@ -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]); }; diff --git a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx index f89153e6c0b4..868a5c28b2f1 100644 --- a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx @@ -250,7 +250,7 @@ describe(' - 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( @@ -271,7 +271,10 @@ describe(' - 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 diff --git a/packages/x-data-grid/src/utils/utils.ts b/packages/x-data-grid/src/utils/utils.ts index 623213bd3b49..9edeb5ecd8ed 100644 --- a/packages/x-data-grid/src/utils/utils.ts +++ b/packages/x-data-grid/src/utils/utils.ts @@ -215,3 +215,9 @@ export function deepClone(obj: Record) { * 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); + } +};