diff --git a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx index 916eedbbb3250..c2d48c4c57f76 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx @@ -4,7 +4,6 @@ import { RefObject } from '@mui/x-internals/types'; import { useGridInitialization, useGridInitializeState, - useGridVirtualizer, useGridClipboard, useGridColumnMenu, useGridColumns, @@ -24,7 +23,6 @@ import { useGridRows, useGridRowsPreProcessors, rowsStateInitializer, - useGridRowsMeta, useGridParamsApi, useGridRowSelection, useGridSorting, @@ -79,6 +77,7 @@ import { propsStateInitializer, rowReorderStateInitializer, type GridConfiguration, + useFirstRender, } from '@mui/x-data-grid-pro/internals'; import { useGridSelector } from '@mui/x-data-grid-pro'; import { GridPrivateApiPremium } from '../models/gridApiPremium'; @@ -202,7 +201,6 @@ export const useDataGridPremiumComponent = ( useGridInitializeState(aiAssistantStateInitializer, apiRef, props); useGridInitializeState(chartsIntegrationStateInitializer, apiRef, props); - useGridVirtualizer(apiRef, props); useGridSidebar(apiRef, props); useGridPivoting(apiRef, props, inProps.columns, inProps.rows); useGridRowGrouping(apiRef, props); @@ -231,7 +229,6 @@ export const useDataGridPremiumComponent = ( useGridColumnReorder(apiRef, props); useGridColumnResize(apiRef, props); useGridPagination(apiRef, props); - useGridRowsMeta(apiRef, props); useGridRowReorder(apiRef, props); useGridScroll(apiRef, props); useGridInfiniteLoader(apiRef, props); @@ -254,6 +251,9 @@ export const useDataGridPremiumComponent = ( useGridPivotingExportState(apiRef); // Should be the last thing to run, because all pre-processors should have been registered by now. + useFirstRender(() => { + apiRef.current.runAppliersForPendingProcessors(); + }); React.useEffect(() => { apiRef.current.runAppliersForPendingProcessors(); }); diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts index d52c5d00045ea..0b833f515abe8 100644 --- a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts +++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts @@ -10,6 +10,7 @@ import { gridRenderContextSelector, gridVisibleColumnFieldsSelector, gridSortModelSelector, + gridRowMaximumTreeDepthSelector, } from '@mui/x-data-grid-pro'; import { useGridRegisterPipeProcessor, @@ -89,126 +90,128 @@ export const useGridAggregation = ( ); const abortControllerRef = React.useRef(null); - const applyAggregation = React.useCallback( - (reason?: 'filter' | 'sort') => { - // Abort previous if any - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - const abortController = new AbortController(); - abortControllerRef.current = abortController; - - const aggregationRules = getAggregationRules( - gridColumnLookupSelector(apiRef), - gridAggregationModelSelector(apiRef), - props.aggregationFunctions, - !!props.dataSource, - ); - const aggregatedFields = Object.keys(aggregationRules); - const currentAggregationLookup = gridAggregationLookupSelector(apiRef); - const needsSorting = shouldApplySorting(aggregationRules, aggregatedFields); - if (reason === 'sort' && !needsSorting) { - // no need to re-apply aggregation on `sortedRowsSet` if sorting is not needed - return; - } + const applyAggregation = React.useCallback(() => { + // Abort previous if we're proceeding + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + const abortController = new AbortController(); + abortControllerRef.current = abortController; - const renderContext = gridRenderContextSelector(apiRef); - const visibleColumns = gridVisibleColumnFieldsSelector(apiRef); + const aggregationRules = getAggregationRules( + gridColumnLookupSelector(apiRef), + gridAggregationModelSelector(apiRef), + props.aggregationFunctions, + !!props.dataSource, + ); + const aggregatedFields = Object.keys(aggregationRules); + const currentAggregationLookup = gridAggregationLookupSelector(apiRef); + + const renderContext = gridRenderContextSelector(apiRef); + const visibleColumns = gridVisibleColumnFieldsSelector(apiRef); + + const chunks: string[][] = []; + const sortedAggregatedFields = gridSortModelSelector(apiRef) + .map((s) => s.field) + .filter((field) => aggregatedFields.includes(field)); + const visibleAggregatedFields = visibleColumns + .slice(renderContext.firstColumnIndex, renderContext.lastColumnIndex + 1) + .filter((field) => aggregatedFields.includes(field)); + const visibleAggregatedFieldsWithSort = [ + ...visibleAggregatedFields, + ...sortedAggregatedFields.filter((field) => !visibleAggregatedFields.includes(field)), + ]; + const hasAggregatedSortedField = + gridRowMaximumTreeDepthSelector(apiRef) > 1 && sortedAggregatedFields.length > 0; + + if (visibleAggregatedFields.length > 0) { + chunks.push(visibleAggregatedFieldsWithSort); + } + const otherAggregatedFields = aggregatedFields.filter( + (field) => !visibleAggregatedFieldsWithSort.includes(field), + ); - const chunks: string[][] = []; - const visibleAggregatedFields = visibleColumns - .slice(renderContext.firstColumnIndex, renderContext.lastColumnIndex + 1) - .filter((field) => aggregatedFields.includes(field)); - if (visibleAggregatedFields.length > 0) { - chunks.push(visibleAggregatedFields); - } - const otherAggregatedFields = aggregatedFields.filter( - (field) => !visibleAggregatedFields.includes(field), - ); + const chunkSize = 20; // columns per chunk + for (let i = 0; i < otherAggregatedFields.length; i += chunkSize) { + chunks.push(otherAggregatedFields.slice(i, i + chunkSize)); + } - const chunkSize = 20; // columns per chunk - for (let i = 0; i < otherAggregatedFields.length; i += chunkSize) { - chunks.push(otherAggregatedFields.slice(i, i + chunkSize)); - } + let chunkIndex = 0; + const aggregationLookup: GridAggregationLookup = {}; + let chunkStartTime = performance.now(); + const timeLimit = 1000 / 120; - let chunkIndex = 0; - const aggregationLookup: GridAggregationLookup = {}; - let chunkStartTime = performance.now(); - const timeLimit = 1000 / 120; + const processChunk = () => { + if (abortController.signal.aborted) { + return; + } - const processChunk = () => { - if (abortController.signal.aborted) { - return; - } + const currentChunk = chunks[chunkIndex]; + if (!currentChunk) { + apiRef.current.publishEvent('aggregationLookupSet'); + abortControllerRef.current = null; + return; + } - const currentChunk = chunks[chunkIndex]; - if (!currentChunk) { - const sortModel = gridSortModelSelector(apiRef).map((s) => s.field); - const hasAggregatedSorting = sortModel.some((field) => aggregationRules[field]); - if (hasAggregatedSorting) { - apiRef.current.applySorting(); - } - abortControllerRef.current = null; - return; + const applySorting = shouldApplySorting(aggregationRules, currentChunk); + + // createAggregationLookup now RETURNS new partial lookup + const partialLookup = createAggregationLookup({ + apiRef, + getAggregationPosition: props.getAggregationPosition, + aggregatedFields: currentChunk, + aggregationRules, + aggregationRowsScope: props.aggregationRowsScope, + isDataSource: !!props.dataSource, + applySorting, + }); + + for (const key of Object.keys(partialLookup)) { + for (const field of Object.keys(partialLookup[key])) { + aggregationLookup[key] ??= {}; + aggregationLookup[key][field] = partialLookup[key][field]; } + } - const applySorting = shouldApplySorting(aggregationRules, currentChunk); - - // createAggregationLookup now RETURNS new partial lookup - const partialLookup = createAggregationLookup({ - apiRef, - getAggregationPosition: props.getAggregationPosition, - aggregatedFields: currentChunk, - aggregationRules, - aggregationRowsScope: props.aggregationRowsScope, - isDataSource: !!props.dataSource, - applySorting, - }); - - for (const key of Object.keys(partialLookup)) { - for (const field of Object.keys(partialLookup[key])) { - aggregationLookup[key] ??= {}; - aggregationLookup[key][field] = partialLookup[key][field]; - } - } + apiRef.current.setState((state) => ({ + ...state, + aggregation: { ...state.aggregation, lookup: { ...aggregationLookup } }, + })); - apiRef.current.setState((state) => ({ - ...state, - aggregation: { ...state.aggregation, lookup: { ...aggregationLookup } }, - })); + if (chunkIndex === 0 && hasAggregatedSortedField) { + apiRef.current.applySorting(); + } - chunkIndex += 1; + chunkIndex += 1; - if (performance.now() - chunkStartTime < timeLimit) { - processChunk(); - return; - } + if (performance.now() - chunkStartTime < timeLimit) { + processChunk(); + return; + } - setTimeout(() => { - chunkStartTime = performance.now(); - processChunk(); - }, 0); - }; + setTimeout(() => { + chunkStartTime = performance.now(); + processChunk(); + }, 0); + }; - processChunk(); + processChunk(); - // processChunk() does nothing if there are no aggregated fields - // make sure that the lookup is empty in this case - if (aggregatedFields.length === 0 && !isObjectEmpty(currentAggregationLookup)) { - apiRef.current.setState((state) => ({ - ...state, - aggregation: { ...state.aggregation, lookup: {} }, - })); - } - }, - [ - apiRef, - props.getAggregationPosition, - props.aggregationFunctions, - props.aggregationRowsScope, - props.dataSource, - ], - ); + // processChunk() does nothing if there are no aggregated fields + // make sure that the lookup is empty in this case + if (aggregatedFields.length === 0 && !isObjectEmpty(currentAggregationLookup)) { + apiRef.current.setState((state) => ({ + ...state, + aggregation: { ...state.aggregation, lookup: {} }, + })); + } + }, [ + apiRef, + props.getAggregationPosition, + props.aggregationFunctions, + props.aggregationRowsScope, + props.dataSource, + ]); React.useEffect(() => { return () => { @@ -219,7 +222,7 @@ export const useGridAggregation = ( }; }, []); - const deferredApplyAggregation = useRunOncePerLoop(applyAggregation); + const { schedule: deferredApplyAggregation } = useRunOncePerLoop(applyAggregation); const aggregationApi: GridAggregationApi = { setAggregationModel, @@ -285,7 +288,32 @@ export const useGridAggregation = ( useGridEvent(apiRef, 'aggregationModelChange', checkAggregationRulesDiff); useGridEvent(apiRef, 'columnsChange', checkAggregationRulesDiff); useGridEvent(apiRef, 'filteredRowsSet', deferredApplyAggregation); - useGridEvent(apiRef, 'sortedRowsSet', () => deferredApplyAggregation('sort')); + + const lastSortModel = React.useRef(gridSortModelSelector(apiRef)); + useGridEvent(apiRef, 'sortedRowsSet', () => { + const sortModel = gridSortModelSelector(apiRef); + if (lastSortModel.current === sortModel) { + return; + } + lastSortModel.current = sortModel; + + const aggregationRules = getAggregationRules( + gridColumnLookupSelector(apiRef), + gridAggregationModelSelector(apiRef), + props.aggregationFunctions, + !!props.dataSource, + ); + const aggregatedFields = Object.keys(aggregationRules); + if (!aggregatedFields.length) { + return; + } + const needsSorting = shouldApplySorting(aggregationRules, aggregatedFields); + if (!needsSorting) { + return; + } + + deferredApplyAggregation(); + }); /** * EFFECTS @@ -295,4 +323,10 @@ export const useGridAggregation = ( apiRef.current.setAggregationModel(props.aggregationModel); } }, [apiRef, props.aggregationModel]); + + React.useEffect(() => { + if (props.getAggregationPosition !== undefined) { + deferredApplyAggregation(); + } + }, [deferredApplyAggregation, props.getAggregationPosition]); }; diff --git a/packages/x-data-grid-premium/src/hooks/features/chartsIntegration/useGridChartsIntegration.tsx b/packages/x-data-grid-premium/src/hooks/features/chartsIntegration/useGridChartsIntegration.tsx index 1a93e6bd926d0..db87d91102bfe 100644 --- a/packages/x-data-grid-premium/src/hooks/features/chartsIntegration/useGridChartsIntegration.tsx +++ b/packages/x-data-grid-premium/src/hooks/features/chartsIntegration/useGridChartsIntegration.tsx @@ -879,6 +879,11 @@ export const useGridChartsIntegration = ( 'sortedRowsSet', runIf(isChartsIntegrationAvailable, () => debouncedHandleRowDataUpdate(syncedChartIds)), ); + useGridEvent( + apiRef, + 'aggregationLookupSet', + runIf(isChartsIntegrationAvailable, () => debouncedHandleRowDataUpdate(syncedChartIds)), + ); React.useEffect(() => { if (!activeChartId && availableChartIds.length > 0) { diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx index bb3dcc8f0a123..bbe70c85aadeb 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx @@ -4,7 +4,6 @@ import { RefObject } from '@mui/x-internals/types'; import { useGridInitialization, useGridInitializeState, - useGridVirtualizer, useGridClipboard, useGridColumnMenu, useGridColumns, @@ -24,7 +23,6 @@ import { useGridRows, useGridRowsPreProcessors, rowsStateInitializer, - useGridRowsMeta, useGridParamsApi, useGridRowSelection, useGridSorting, @@ -56,6 +54,7 @@ import { listViewStateInitializer, propsStateInitializer, GridConfiguration, + useFirstRender, } from '@mui/x-data-grid/internals'; import { GridPrivateApiPro } from '../models/gridApiPro'; import { DataGridProProcessedProps } from '../models/dataGridProProps'; @@ -153,7 +152,6 @@ export const useDataGridProComponent = ( useGridInitializeState(rowsMetaStateInitializer, apiRef, props); useGridInitializeState(listViewStateInitializer, apiRef, props); - useGridVirtualizer(apiRef, props); useGridHeaderFiltering(apiRef, props); useGridTreeData(apiRef, props); useGridKeyboardNavigation(apiRef, props); @@ -176,7 +174,6 @@ export const useDataGridProComponent = ( useGridColumnReorder(apiRef, props); useGridColumnResize(apiRef, props); useGridPagination(apiRef, props); - useGridRowsMeta(apiRef, props); useGridRowReorder(apiRef, props); useGridScroll(apiRef, props); useGridInfiniteLoader(apiRef, props); @@ -195,6 +192,9 @@ export const useDataGridProComponent = ( useGridListView(apiRef, props); // Should be the last thing to run, because all pre-processors should have been registered by now. + useFirstRender(() => { + apiRef.current.runAppliersForPendingProcessors(); + }); React.useEffect(() => { apiRef.current.runAppliersForPendingProcessors(); }); diff --git a/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts b/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts index 9e5295b05c3a5..9bfeac7bd88e3 100644 --- a/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts +++ b/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts @@ -103,6 +103,10 @@ export const useGridColumnPinningPreProcessors = ( apiRef.current.caches.columnPinning.orderedFieldsBeforePinningColumns = newOrderedFieldsBeforePinningColumns; } else { + if (allPinnedColumns.length === 0) { + prevAllPinnedColumns.current = allPinnedColumns; + return columnsState; + } newOrderedFields = [...columnsState.orderedFields]; apiRef.current.caches.columnPinning.orderedFieldsBeforePinningColumns = [ ...columnsState.orderedFields, diff --git a/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts b/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts index 77fa4167632da..0aa02d7b47bca 100644 --- a/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts +++ b/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts @@ -257,8 +257,9 @@ export const useGridDetailPanel = ( const updateCachesIfNeeded = React.useCallback(() => { if ( - props.getDetailPanelContent === previousGetDetailPanelContentProp.current && - props.getDetailPanelHeight === previousGetDetailPanelHeightProp.current + (props.getDetailPanelContent === previousGetDetailPanelContentProp.current && + props.getDetailPanelHeight === previousGetDetailPanelHeightProp.current) || + !props.getDetailPanelContent ) { return; } diff --git a/packages/x-data-grid-pro/src/tests/detailPanel.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/detailPanel.DataGridPro.test.tsx index 5dce5f5426270..a093718f20249 100644 --- a/packages/x-data-grid-pro/src/tests/detailPanel.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/detailPanel.DataGridPro.test.tsx @@ -326,7 +326,7 @@ describe(' - Detail panel', () => { // from React 19 it is: // 2x during state initialization // + 2x when sortedRowsSet is fired - const expectedCallCount = reactMajor >= 19 ? 8 : 12; + const expectedCallCount = reactMajor >= 19 ? 6 : 10; expect(getDetailPanelContent.callCount).to.equal(expectedCallCount); await user.click(screen.getByRole('button', { name: 'Expand' })); @@ -368,7 +368,7 @@ describe(' - Detail panel', () => { // from React 19 it is: // 2x during state initialization // + 2x when sortedRowsSet is fired - const expectedCallCount = reactMajor >= 19 ? 8 : 12; + const expectedCallCount = reactMajor >= 19 ? 6 : 10; expect(getDetailPanelHeight.callCount).to.equal(expectedCallCount); await user.click(screen.getByRole('button', { name: 'Expand' })); @@ -441,7 +441,7 @@ describe(' - Detail panel', () => { // from React 19 it is: // 1x during state initialization // + 1x when sortedRowsSet is fired - const expectedCallCount = reactMajor >= 19 ? 4 : 6; + const expectedCallCount = reactMajor >= 19 ? 3 : 5; expect(getDetailPanelHeight.callCount).to.equal(expectedCallCount); expect(getDetailPanelHeight.lastCall.args[0].id).to.equal(0); diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx index 47f81e57e94bd..ed21e2eb64871 100644 --- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx @@ -811,7 +811,7 @@ describe(' - Row selection', () => { />, ); - expect(onRowSelectionModelChange.callCount).to.equal(4); + expect(onRowSelectionModelChange.callCount).to.equal(3); expect(onRowSelectionModelChange.lastCall.args[0]).to.deep.equal( includeRowSelection([2, 3, 4, 5, 6, 7, 1]), ); diff --git a/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx b/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx index d799bc93ebf1f..41b1f58805b5c 100644 --- a/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx +++ b/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx @@ -1,10 +1,10 @@ 'use client'; import * as React from 'react'; import { RefObject } from '@mui/x-internals/types'; +import { useFirstRender } from '@mui/x-internals/useFirstRender'; import { DataGridProcessedProps } from '../models/props/DataGridProps'; import { GridPrivateApiCommunity } from '../models/api/gridApiCommunity'; import { useGridInitialization } from '../hooks/core/useGridInitialization'; -import { useGridVirtualizer } from '../hooks/core/useGridVirtualizer'; import { useGridInitializeState } from '../hooks/utils/useGridInitializeState'; import { useGridClipboard } from '../hooks/features/clipboard/useGridClipboard'; import { @@ -42,7 +42,7 @@ import { dimensionsStateInitializer, useGridDimensions, } from '../hooks/features/dimensions/useGridDimensions'; -import { rowsMetaStateInitializer, useGridRowsMeta } from '../hooks/features/rows/useGridRowsMeta'; +import { rowsMetaStateInitializer } from '../hooks/features/rows/useGridRowsMeta'; import { useGridStatePersistence } from '../hooks/features/statePersistence/useGridStatePersistence'; import { useGridColumnSpanning } from '../hooks/features/columns/useGridColumnSpanning'; import { @@ -105,7 +105,6 @@ export const useDataGridComponent = ( useGridInitializeState(rowsMetaStateInitializer, apiRef, props); useGridInitializeState(listViewStateInitializer, apiRef, props); - useGridVirtualizer(apiRef, props); useGridKeyboardNavigation(apiRef, props); useGridRowSelection(apiRef, props); useGridColumns(apiRef, props); @@ -122,7 +121,6 @@ export const useDataGridComponent = ( useGridDensity(apiRef, props); useGridColumnResize(apiRef, props); useGridPagination(apiRef, props); - useGridRowsMeta(apiRef, props); useGridScroll(apiRef, props); useGridColumnMenu(apiRef); useGridCsvExport(apiRef, props); @@ -136,6 +134,9 @@ export const useDataGridComponent = ( useGridDataSource(apiRef, props); // Should be the last thing to run, because all pre-processors should have been registered by now. + useFirstRender(() => { + apiRef.current.runAppliersForPendingProcessors(); + }); React.useEffect(() => { apiRef.current.runAppliersForPendingProcessors(); }); diff --git a/packages/x-data-grid/src/components/GridRow.tsx b/packages/x-data-grid/src/components/GridRow.tsx index 93b11e2f93839..a35849acd5d30 100644 --- a/packages/x-data-grid/src/components/GridRow.tsx +++ b/packages/x-data-grid/src/components/GridRow.tsx @@ -278,7 +278,14 @@ const GridRow = forwardRef(function GridRow(props, } return rowStyle; - }, [isNotVisible, rowHeight, styleProp, heightEntry, rootProps.rowSpacingType]); + }, [ + isNotVisible, + rowHeight, + styleProp, + heightEntry.spacingTop, + heightEntry.spacingBottom, + rootProps.rowSpacingType, + ]); // HACK: Sometimes, the rowNode has already been removed from the state but the row is still rendered. if (!rowNode) { diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index 113a3c03c9546..672cea9299d68 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -11,6 +11,7 @@ import { useRtl } from '@mui/system/RtlProvider'; import { forwardRef } from '@mui/x-internals/forwardRef'; import { useStore } from '@mui/x-internals/store'; import { Rowspan } from '@mui/x-virtualizer'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { doesSupportPreventScroll } from '../../utils/doesSupportPreventScroll'; import { getDataGridUtilityClass, gridClasses } from '../../constants/gridClasses'; import { @@ -350,7 +351,7 @@ const GridCell = forwardRef(function GridCell(pro return cellStyle; }, [width, isNotVisible, styleProp, pinnedOffset, pinnedPosition, isRtl, rowSpan]); - React.useEffect(() => { + useEnhancedEffect(() => { if (!hasFocus || cellMode === GridCellModes.Edit) { return; } diff --git a/packages/x-data-grid/src/components/virtualization/GridVirtualScroller.tsx b/packages/x-data-grid/src/components/virtualization/GridVirtualScroller.tsx index 7e7cbac3c6bc5..02ab8358bc139 100644 --- a/packages/x-data-grid/src/components/virtualization/GridVirtualScroller.tsx +++ b/packages/x-data-grid/src/components/virtualization/GridVirtualScroller.tsx @@ -29,6 +29,7 @@ import type { GridLoadingOverlayVariant, } from '../../hooks/features/overlays/gridOverlaysInterfaces'; import { GridApiCommunity } from '../../models/api/gridApiCommunity'; +import { useGridVirtualizer } from '../../hooks/core/useGridVirtualizer'; type OwnerState = Pick & { hasScrollX: boolean; @@ -100,7 +101,7 @@ function GridVirtualScroller(props: GridVirtualScrollerProps) { }; const classes = useUtilityClasses(ownerState); - const virtualScroller = apiRef.current.virtualizer.api.useVirtualization().getters; + const virtualScroller = useGridVirtualizer().api.getters; const { getContainerProps, diff --git a/packages/x-data-grid/src/components/virtualization/GridVirtualScrollerRenderZone.tsx b/packages/x-data-grid/src/components/virtualization/GridVirtualScrollerRenderZone.tsx index b5591f5200974..b49cefa299748 100644 --- a/packages/x-data-grid/src/components/virtualization/GridVirtualScrollerRenderZone.tsx +++ b/packages/x-data-grid/src/components/virtualization/GridVirtualScrollerRenderZone.tsx @@ -37,7 +37,7 @@ const GridVirtualScrollerRenderZone = forwardRef< const apiRef = useGridPrivateApiContext(); const rootProps = useGridRootProps(); const classes = useUtilityClasses(rootProps); - const offsetTop = apiRef.current.virtualizer.api.useVirtualization().getters.getOffsetTop(); + const offsetTop = apiRef.current.virtualizer.api.getters.getOffsetTop(); return ( ( apiRef: RefObject, props: GridStateProps, ) => { + const isFirstRender = React.useRef(true); React.useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } apiRef.current.setState((state: GridStateCommunity) => ({ ...state, props: { diff --git a/packages/x-data-grid/src/hooks/core/useGridVirtualizer.tsx b/packages/x-data-grid/src/hooks/core/useGridVirtualizer.tsx index d9f242b88c0dd..83788235af184 100644 --- a/packages/x-data-grid/src/hooks/core/useGridVirtualizer.tsx +++ b/packages/x-data-grid/src/hooks/core/useGridVirtualizer.tsx @@ -2,13 +2,17 @@ import * as React from 'react'; import useLazyRef from '@mui/utils/useLazyRef'; import useEventCallback from '@mui/utils/useEventCallback'; import { useRtl } from '@mui/system/RtlProvider'; -import { RefObject } from '@mui/x-internals/types'; import { roundToDecimalPlaces } from '@mui/x-internals/math'; import { lruMemoize } from '@mui/x-internals/lruMemoize'; import { useStoreEffect } from '@mui/x-internals/store'; -import { useVirtualizer, Dimensions, VirtualizerParams } from '@mui/x-virtualizer'; +import { + useVirtualizer, + Dimensions, + VirtualizerParams, + Virtualization, + EMPTY_RENDER_CONTEXT, +} from '@mui/x-virtualizer'; import { useFirstRender } from '../utils/useFirstRender'; -import { GridPrivateApiCommunity } from '../../models/api/gridApiCommunity'; import { GridStateColDef } from '../../models/colDef/gridColDef'; import { createSelector } from '../../utils/createSelector'; import { useGridSelector } from '../utils/useGridSelector'; @@ -25,7 +29,6 @@ import { } from '../features/columns/gridColumnsSelector'; import { gridPinnedRowsSelector, gridRowCountSelector } from '../features/rows/gridRowsSelector'; import { useGridVisibleRows } from '../utils/useGridVisibleRows'; -import { DataGridProcessedProps } from '../../models/props/DataGridProps'; import { gridPaginationSelector } from '../features/pagination'; import { gridFocusedVirtualCellSelector } from '../features/virtualization/gridFocusedVirtualCellSelector'; import { gridRowSelectionManagerSelector } from '../features/rowSelection'; @@ -37,12 +40,10 @@ import { } from '../features/rows/gridRowsUtils'; import { getTotalHeaderHeight } from '../features/columns/gridColumnsUtils'; import { useGridOverlays } from '../features/overlays/useGridOverlays'; - -function identity(x: T) { - return x; -} - -type RootProps = DataGridProcessedProps; +import { useGridRootProps } from '../utils/useGridRootProps'; +import { useGridPrivateApiContext } from '../utils/useGridPrivateApiContext'; +import { useGridRowsMeta } from '../features/rows/useGridRowsMeta'; +import { eslintUseValue } from '../../utils/utils'; const columnsTotalWidthSelector = createSelector( gridVisibleColumnDefinitionsSelector, @@ -83,11 +84,10 @@ const addGridDimensionsCreator = () => /** * Virtualizer setup */ -export function useGridVirtualizer( - apiRef: RefObject, - rootProps: RootProps, -): void { +export function useGridVirtualizer() { const isRtl = useRtl(); + const rootProps = useGridRootProps(); + const apiRef = useGridPrivateApiContext(); const { listView } = rootProps; const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); @@ -159,11 +159,13 @@ export function useGridVirtualizer( const { getRowHeight, getEstimatedRowHeight, getRowSpacing } = rootProps; // - const focusedVirtualCell = useGridSelector(apiRef, gridFocusedVirtualCellSelector); - const RowSlot = rootProps.slots.row; const rowSlotProps = rootProps.slotProps?.row; + const focusedVirtualCell = useGridSelector(apiRef, gridFocusedVirtualCellSelector); + // We need it to trigger a new render, but rowsMeta needs access to the latest value, hence we cannot pass it to the focusedVirtualCell callback in the virtualizer params + eslintUseValue(focusedVirtualCell); + const virtualizer = useVirtualizer({ refs: { container: apiRef.current.mainElementRef, @@ -252,7 +254,7 @@ export function useGridVirtualizer( ), virtualizeColumnsWithAutoRowHeight: rootProps.virtualizeColumnsWithAutoRowHeight, - focusedVirtualCell: useEventCallback(() => focusedVirtualCell), + focusedVirtualCell: useEventCallback(() => gridFocusedVirtualCellSelector(apiRef)), resizeThrottleMs: rootProps.resizeThrottleMs, onResize: useEventCallback((size) => apiRef.current.publishEvent('resize', size)), @@ -339,6 +341,9 @@ export function useGridVirtualizer( }); useStoreEffect(virtualizer.store, Dimensions.selectors.dimensions, (_, dimensions) => { + if (!dimensions.isReady) { + return; + } apiRef.current.setState((gridState) => ({ ...gridState, dimensions: addGridDimensions( @@ -351,17 +356,23 @@ export function useGridVirtualizer( })); }); - useStoreEffect(virtualizer.store, identity, (_, state) => { - if (state.rowsMeta !== apiRef.current.state.rowsMeta) { + useStoreEffect(virtualizer.store, Dimensions.selectors.rowsMeta, (_, rowsMeta) => { + if (rowsMeta !== apiRef.current.state.rowsMeta) { apiRef.current.setState((gridState) => ({ ...gridState, - rowsMeta: state.rowsMeta, + rowsMeta, })); } - if (state.virtualization !== apiRef.current.state.virtualization) { + }); + + useStoreEffect(virtualizer.store, Virtualization.selectors.store, (_, virtualization) => { + if (virtualization.renderContext === EMPTY_RENDER_CONTEXT) { + return; + } + if (virtualization !== apiRef.current.state.virtualization) { apiRef.current.setState((gridState) => ({ ...gridState, - virtualization: state.virtualization, + virtualization, })); } }); @@ -369,4 +380,8 @@ export function useGridVirtualizer( apiRef.current.register('private', { virtualizer, }); + + useGridRowsMeta(apiRef, rootProps); + + return virtualizer; } diff --git a/packages/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGrouping.ts b/packages/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGrouping.ts index 3c1278e25dc7e..d698864ceee6b 100644 --- a/packages/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGrouping.ts +++ b/packages/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGrouping.ts @@ -27,7 +27,10 @@ export const columnGroupsStateInitializer: GridStateInitializer< lastColumnGroupingModel: props.columnGroupingModel, }; if (!props.columnGroupingModel) { - return state; + return { + ...state, + columnGrouping: undefined, + }; } const columnFields = gridColumnFieldsSelector(apiRef); @@ -113,6 +116,9 @@ export const useGridColumnGrouping = ( const updateColumnGroupingState = React.useCallback( (columnGroupingModel: GridColumnGroupingModel | undefined) => { + if (!columnGroupingModel && !apiRef.current.caches.columnGrouping.lastColumnGroupingModel) { + return; + } apiRef.current.caches.columnGrouping.lastColumnGroupingModel = columnGroupingModel; // @ts-expect-error Move this logic to `Pro` package const pinnedColumns = apiRef.current.getPinnedColumns?.() ?? {}; diff --git a/packages/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index a1921f528f507..2a7509ca5bb3a 100644 --- a/packages/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -12,12 +12,15 @@ import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; * @requires useGridParamsApi (method) */ export const useGridColumnSpanning = (apiRef: RefObject) => { - const virtualizer = apiRef.current.virtualizer; - - const resetColSpan: GridColumnSpanningPrivateApi['resetColSpan'] = virtualizer.api.resetColSpan; - const getCellColSpanInfo: GridColumnSpanningApi['unstable_getCellColSpanInfo'] = - virtualizer.api.getCellColSpanInfo; - const calculateColSpan = virtualizer.api.calculateColSpan; + const resetColSpan: GridColumnSpanningPrivateApi['resetColSpan'] = () => { + return apiRef.current.virtualizer.api.resetColSpan(); + }; + const getCellColSpanInfo: GridColumnSpanningApi['unstable_getCellColSpanInfo'] = (...params) => { + return apiRef.current.virtualizer.api.getCellColSpanInfo(...params); + }; + const calculateColSpan: GridColumnSpanningPrivateApi['calculateColSpan'] = (...params) => { + apiRef.current.virtualizer.api.calculateColSpan(...params); + }; const columnSpanningPublicApi: GridColumnSpanningApi = { unstable_getCellColSpanInfo: getCellColSpanInfo, diff --git a/packages/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts b/packages/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts index d11f339a6490d..9092517a9a39a 100644 --- a/packages/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts +++ b/packages/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts @@ -74,18 +74,21 @@ export const dimensionsStateInitializer: GridStateInitializer = ( const dimensions = EMPTY_DIMENSIONS; const density = gridDensityFactorSelector(apiRef); + const dimensionsWithStatic = { + ...dimensions, + ...getStaticDimensions( + props, + apiRef, + density, + gridVisiblePinnedColumnDefinitionsSelector(apiRef), + ), + }; + + apiRef.current.store.state.dimensions = dimensionsWithStatic; return { ...state, - dimensions: { - ...dimensions, - ...getStaticDimensions( - props, - apiRef, - density, - gridVisiblePinnedColumnDefinitionsSelector(apiRef), - ), - }, + dimensions: dimensionsWithStatic, }; }; @@ -105,10 +108,6 @@ const columnsTotalWidthSelector = createSelector( ); export function useGridDimensions(apiRef: RefObject, props: RootProps) { - const virtualizer = apiRef.current.virtualizer; - const updateDimensions = virtualizer.api.updateDimensions; - const getViewportPageSize = virtualizer.api.getViewportPageSize; - const getRootDimensions = React.useCallback(() => gridDimensionsSelector(apiRef), [apiRef]); const apiPublic: GridDimensionsApi = { @@ -116,8 +115,12 @@ export function useGridDimensions(apiRef: RefObject, pr }; const apiPrivate: GridDimensionsPrivateApi = { - updateDimensions, - getViewportPageSize, + updateDimensions: () => { + return apiRef.current.virtualizer.api.updateDimensions(); + }, + getViewportPageSize: () => { + return apiRef.current.virtualizer.api.getViewportPageSize(); + }, }; useGridApiMethod(apiRef, apiPublic, 'public'); @@ -171,6 +174,10 @@ export function useGridDimensions(apiRef: RefObject, pr apiRef.current.store, (s) => s.dimensions, (previous, next) => { + if (!next.isReady) { + return; + } + if (apiRef.current.rootElementRef.current) { setCSSVariables(apiRef.current.rootElementRef.current, next); } diff --git a/packages/x-data-grid/src/hooks/features/filter/useGridFilter.tsx b/packages/x-data-grid/src/hooks/features/filter/useGridFilter.tsx index 4d774bfcbf924..9f5498f6f55f7 100644 --- a/packages/x-data-grid/src/hooks/features/filter/useGridFilter.tsx +++ b/packages/x-data-grid/src/hooks/features/filter/useGridFilter.tsx @@ -94,6 +94,7 @@ export const useGridFilter = ( | 'disableColumnFilter' | 'disableEval' | 'ignoreDiacritics' + | 'signature' >, configuration: GridConfiguration, ): void => { @@ -546,7 +547,9 @@ export const useGridFilter = ( * 1ST RENDER */ useFirstRender(() => { - updateFilteredRows(); + if (props.signature === 'DataGrid') { + updateFilteredRows(); + } }); /** diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 496d4a1cf9d04..4027e7ec9d46d 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -874,11 +874,6 @@ export const useGridRowSelection = ( apiRef.current.setRowSelectionModel(propRowSelectionModel); }); - useGridEvent( - apiRef, - 'sortedRowsSet', - runIfRowSelectionIsEnabled(() => removeOutdatedSelection(true)), - ); useGridEvent( apiRef, 'filteredRowsSet', 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 9c64465523dad..d0f635598b9b1 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -15,8 +15,7 @@ import type { GridStateInitializer } from '../../utils/useGridInitializeState'; import { getUnprocessedRange, isRowContextInitialized, getCellValue } from './gridRowSpanningUtils'; import { useGridEvent } from '../../utils/useGridEvent'; import { runIf } from '../../../utils/utils'; -import { gridPageSizeSelector } from '../pagination'; -import { gridDataRowIdsSelector } from './gridRowsSelector'; +import { useRunOncePerLoop } from '../../utils/useRunOncePerLoop'; export interface GridRowSpanningState extends RowSpanningState {} @@ -30,13 +29,6 @@ const EMPTY_CACHES: RowSpanningState['caches'] = { const EMPTY_RANGE: RowRange = { firstRowIndex: 0, lastRowIndex: 0 }; const EMPTY_STATE = { caches: EMPTY_CACHES, processedRange: EMPTY_RANGE }; -/** - * Default number of rows to process during state initialization to avoid flickering. - * Number `20` is arbitrarily chosen to be large enough to cover most of the cases without - * compromising performance. - */ -const DEFAULT_ROWS_TO_PROCESS = 20; - const computeRowSpanningState = ( apiRef: RefObject, colDefs: GridColDef[], @@ -150,83 +142,15 @@ const computeRowSpanningState = ( return { caches: { spannedCells, hiddenCells, hiddenCellOriginMap }, processedRange }; }; -const getInitialRangeToProcess = ( - props: Pick, - apiRef: React.RefObject, -) => { - const rowCount = gridDataRowIdsSelector(apiRef).length; - - if (props.pagination) { - const pageSize = gridPageSizeSelector(apiRef); - let paginationLastRowIndex = DEFAULT_ROWS_TO_PROCESS; - if (pageSize > 0) { - paginationLastRowIndex = pageSize - 1; - } - return { - firstRowIndex: 0, - lastRowIndex: Math.min(paginationLastRowIndex, rowCount), - }; - } - - return { - firstRowIndex: 0, - lastRowIndex: Math.min(DEFAULT_ROWS_TO_PROCESS, rowCount), - }; -}; - /** * @requires columnsStateInitializer (method) - should be initialized before * @requires rowsStateInitializer (method) - should be initialized before * @requires filterStateInitializer (method) - should be initialized before */ -export const rowSpanningStateInitializer: GridStateInitializer = (state, props, apiRef) => { - if (!props.rowSpanning) { - return { - ...state, - rowSpanning: EMPTY_STATE, - }; - } - - 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 = getInitialRangeToProcess(props, apiRef); - const rows = rowIds.map((id) => ({ - id, - model: dataRowIdToModelLookup[id!], - })) as GridRowEntry[]; - const colDefs = orderedFields.map((field) => columnsLookup[field!]) as GridColDef[]; - - const rowSpanning = computeRowSpanningState( - apiRef, - colDefs, - rows, - rangeToProcess, - rangeToProcess, - true, - ); - +export const rowSpanningStateInitializer: GridStateInitializer = (state) => { return { ...state, - rowSpanning, + rowSpanning: EMPTY_STATE, }; }; @@ -234,12 +158,11 @@ export const useGridRowSpanning = ( apiRef: RefObject, props: Pick, ): void => { - const store = apiRef.current.virtualizer.store; - const updateRowSpanningState = React.useCallback( (renderContext: GridRenderContext, resetState: boolean = false) => { + const store = apiRef.current.virtualizer.store; const { range, rows: visibleRows } = getVisibleRows(apiRef); - if (resetState && store.getSnapshot().rowSpanning !== EMPTY_STATE) { + if (resetState) { store.set('rowSpanning', EMPTY_STATE); } @@ -291,7 +214,7 @@ export const useGridRowSpanning = ( store.set('rowSpanning', newState); }, - [apiRef, store], + [apiRef], ); // Reset events trigger a full re-computation of the row spanning state: @@ -300,18 +223,24 @@ export const useGridRowSpanning = ( // - The sorting is applied // - The `paginationModel` is updated // - The rows are updated + const { schedule: deferredUpdateRowSpawnningState, cancel } = + useRunOncePerLoop(updateRowSpanningState); + const resetRowSpanningState = React.useCallback(() => { const renderContext = gridRenderContextSelector(apiRef); if (!isRowContextInitialized(renderContext)) { return; } - updateRowSpanningState(renderContext, true); - }, [apiRef, updateRowSpanningState]); + deferredUpdateRowSpawnningState(renderContext, true); + }, [apiRef, deferredUpdateRowSpawnningState]); useGridEvent( apiRef, 'renderedRowsIntervalChange', - runIf(props.rowSpanning, updateRowSpanningState), + runIf(props.rowSpanning, (renderContext: GridRenderContext) => { + const didHavePendingReset = cancel(); + updateRowSpanningState(renderContext, didHavePendingReset); + }), ); useGridEvent(apiRef, 'sortedRowsSet', runIf(props.rowSpanning, resetRowSpanningState)); @@ -320,6 +249,10 @@ export const useGridRowSpanning = ( useGridEvent(apiRef, 'columnsChange', runIf(props.rowSpanning, resetRowSpanningState)); React.useEffect(() => { + const store = apiRef.current.virtualizer?.store; + if (!store) { + return; + } if (!props.rowSpanning) { if (store.state.rowSpanning !== EMPTY_STATE) { store.set('rowSpanning', EMPTY_STATE); @@ -327,5 +260,5 @@ export const useGridRowSpanning = ( } else if (store.state.rowSpanning.caches === EMPTY_CACHES) { resetRowSpanningState(); } - }, [apiRef, store, resetRowSpanningState, props.rowSpanning]); + }, [apiRef, resetRowSpanningState, props.rowSpanning]); }; diff --git a/packages/x-data-grid/src/hooks/features/sorting/useGridSorting.ts b/packages/x-data-grid/src/hooks/features/sorting/useGridSorting.ts index a40c01f7bd4b1..62d79e2a864ac 100644 --- a/packages/x-data-grid/src/hooks/features/sorting/useGridSorting.ts +++ b/packages/x-data-grid/src/hooks/features/sorting/useGridSorting.ts @@ -64,6 +64,7 @@ export const useGridSorting = ( | 'disableColumnSorting' | 'disableMultipleColumnsSorting' | 'multipleColumnsSortingMode' + | 'signature' >, ) => { const logger = useGridLogger(apiRef, 'useGridSorting'); @@ -373,7 +374,9 @@ export const useGridSorting = ( * 1ST RENDER */ useFirstRender(() => { - apiRef.current.applySorting(); + if (props.signature === 'DataGrid') { + apiRef.current.applySorting(); + } }); /** diff --git a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx index 74e68c918fc22..ab549241b609b 100644 --- a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx +++ b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx @@ -42,7 +42,6 @@ export function useGridVirtualization( apiRef: RefObject, rootProps: RootProps, ): void { - const { virtualizer } = apiRef.current; const { autoHeight, disableVirtualization } = rootProps; /* @@ -50,7 +49,16 @@ export function useGridVirtualization( */ const setVirtualization = (enabled: boolean) => { + const { virtualizer } = apiRef.current; enabled &&= HAS_LAYOUT; + const snapshot = virtualizer.store.getSnapshot(); + if ( + snapshot.virtualization.enabled === enabled && + snapshot.virtualization.enabledForRows === enabled && + snapshot.virtualization.enabledForColumns === enabled + ) { + return; + } virtualizer.store.set('virtualization', { ...virtualizer.store.state.virtualization, enabled, @@ -60,7 +68,12 @@ export function useGridVirtualization( }; const setColumnVirtualization = (enabled: boolean) => { + const { virtualizer } = apiRef.current; enabled &&= HAS_LAYOUT; + const snapshot = virtualizer.store.getSnapshot(); + if (snapshot.virtualization.enabledForColumns === enabled) { + return; + } virtualizer.store.set('virtualization', { ...virtualizer.store.state.virtualization, enabledForColumns: enabled, @@ -74,7 +87,10 @@ export function useGridVirtualization( useGridApiMethod(apiRef, api, 'public'); - const forceUpdateRenderContext = virtualizer.api.forceUpdateRenderContext; + const forceUpdateRenderContext = () => { + const { virtualizer } = apiRef.current; + virtualizer?.api.scheduleUpdateRenderContext(); + }; apiRef.current.register('private', { updateRenderContext: forceUpdateRenderContext, @@ -90,7 +106,10 @@ export function useGridVirtualization( /* eslint-disable react-hooks/exhaustive-deps */ React.useEffect(() => { + if (!apiRef.current.virtualizer) { + return; + } setVirtualization(!rootProps.disableVirtualization); - }, [disableVirtualization, autoHeight]); + }, [apiRef, disableVirtualization, autoHeight]); /* eslint-enable react-hooks/exhaustive-deps */ } diff --git a/packages/x-data-grid/src/hooks/utils/useRunOncePerLoop.ts b/packages/x-data-grid/src/hooks/utils/useRunOncePerLoop.ts index 018e009faf1d6..d10c6c302152b 100644 --- a/packages/x-data-grid/src/hooks/utils/useRunOncePerLoop.ts +++ b/packages/x-data-grid/src/hooks/utils/useRunOncePerLoop.ts @@ -1,38 +1,33 @@ +'use client'; import * as React from 'react'; -export function useRunOncePerLoop void>( - callback: T, - nextFrame: boolean = false, -) { - const scheduledRef = React.useRef(false); +export function useRunOncePerLoop void>(callback: T) { + const scheduledCallbackRef = React.useRef<(...args: any) => void>(null); const schedule = React.useCallback( (...args: Parameters) => { - if (scheduledRef.current) { - return; - } - scheduledRef.current = true; - - const runner = () => { - scheduledRef.current = false; + scheduledCallbackRef.current = () => { + scheduledCallbackRef.current = null; callback(...args); }; + }, + [callback], + ); - if (nextFrame) { - if (typeof requestAnimationFrame === 'function') { - requestAnimationFrame(runner); - } - return; - } + React.useLayoutEffect(() => { + if (scheduledCallbackRef.current) { + scheduledCallbackRef.current(); + } + }); - if (typeof queueMicrotask === 'function') { - queueMicrotask(runner); - } else { - Promise.resolve().then(runner); + return { + schedule, + cancel: () => { + if (scheduledCallbackRef.current) { + scheduledCallbackRef.current = null; + return true; } + return false; }, - [callback, nextFrame], - ); - - return schedule; + }; } diff --git a/packages/x-data-grid/src/models/events/gridEventLookup.ts b/packages/x-data-grid/src/models/events/gridEventLookup.ts index df38a32d24100..6f102e21faa88 100644 --- a/packages/x-data-grid/src/models/events/gridEventLookup.ts +++ b/packages/x-data-grid/src/models/events/gridEventLookup.ts @@ -492,6 +492,11 @@ export interface GridEventLookup * @ignore - do not document */ sortedRowsSet: {}; + /** + * Fired when the aggregations are done + * @ignore - do not document + */ + aggregationLookupSet: {}; /** * Fired when the expansion of a row is changed. Called with a [[GridGroupNode]] object. */ diff --git a/packages/x-internals/src/store/useStoreEffect.ts b/packages/x-internals/src/store/useStoreEffect.ts index 8a298a1ef15d4..8daf7e771c002 100644 --- a/packages/x-internals/src/store/useStoreEffect.ts +++ b/packages/x-internals/src/store/useStoreEffect.ts @@ -36,7 +36,9 @@ function initialize(params?: { subscribe: () => { instance.dispose ??= store.subscribe((state) => { const nextState = selector(state); - instance.effect(previousState, nextState); + if (!Object.is(previousState, nextState)) { + instance.effect(previousState, nextState); + } previousState = nextState; }); }, diff --git a/packages/x-virtualizer/src/features/dimensions.ts b/packages/x-virtualizer/src/features/dimensions.ts index f608720d523b7..1c9065050776a 100644 --- a/packages/x-virtualizer/src/features/dimensions.ts +++ b/packages/x-virtualizer/src/features/dimensions.ts @@ -122,140 +122,148 @@ function useDimensions(store: Store, params: VirtualizerParams, _api: onResize, } = params; - const containerNode = refs.container.current; - - const updateDimensions = React.useCallback(() => { - if (isFirstSizing.current) { - return; - } - - const rootSize = selectors.rootSize(store.state); - const rowsMeta = selectors.rowsMeta(store.state); - - // All the floating point dimensions should be rounded to .1 decimal places to avoid subpixel rendering issues - // https://github.com/mui/mui-x/issues/9550#issuecomment-1619020477 - // https://github.com/mui/mui-x/issues/15721 - const scrollbarSize = measureScrollbarSize(containerNode, params.dimensions.scrollbarSize); - - const topContainerHeight = topPinnedHeight + rowsMeta.pinnedTopRowsTotalHeight; - const bottomContainerHeight = bottomPinnedHeight + rowsMeta.pinnedBottomRowsTotalHeight; + const updateDimensions = React.useCallback( + (firstUpdate?: boolean) => { + if (firstUpdate) { + isFirstSizing.current = false; + } + if (isFirstSizing.current) { + return; + } - const contentSize = { - width: columnsTotalWidth, - height: roundToDecimalPlaces(rowsMeta.currentPageTotalHeight, 1), - }; + const containerNode = refs.container.current; + const rootSize = selectors.rootSize(store.state); + const rowsMeta = selectors.rowsMeta(store.state); - let viewportOuterSize: Size; - let viewportInnerSize: Size; - let hasScrollX = false; - let hasScrollY = false; + // All the floating point dimensions should be rounded to .1 decimal places to avoid subpixel rendering issues + // https://github.com/mui/mui-x/issues/9550#issuecomment-1619020477 + // https://github.com/mui/mui-x/issues/15721 + const scrollbarSize = measureScrollbarSize(containerNode, params.dimensions.scrollbarSize); - if (params.autoHeight) { - hasScrollY = false; - hasScrollX = Math.round(columnsTotalWidth) > Math.round(rootSize.width); + const topContainerHeight = topPinnedHeight + rowsMeta.pinnedTopRowsTotalHeight; + const bottomContainerHeight = bottomPinnedHeight + rowsMeta.pinnedBottomRowsTotalHeight; - viewportOuterSize = { - width: rootSize.width, - height: topContainerHeight + bottomContainerHeight + contentSize.height, - }; - viewportInnerSize = { - width: Math.max(0, viewportOuterSize.width - (hasScrollY ? scrollbarSize : 0)), - height: Math.max(0, viewportOuterSize.height - (hasScrollX ? scrollbarSize : 0)), + const contentSize = { + width: columnsTotalWidth, + height: roundToDecimalPlaces(rowsMeta.currentPageTotalHeight, 1), }; - } else { - viewportOuterSize = { - width: rootSize.width, - height: rootSize.height, - }; - viewportInnerSize = { - width: Math.max(0, viewportOuterSize.width), - height: Math.max(0, viewportOuterSize.height - topContainerHeight - bottomContainerHeight), - }; - - const content = contentSize; - const container = viewportInnerSize; - const hasScrollXIfNoYScrollBar = content.width > container.width; - const hasScrollYIfNoXScrollBar = content.height > container.height; - - if (hasScrollXIfNoYScrollBar || hasScrollYIfNoXScrollBar) { - hasScrollY = hasScrollYIfNoXScrollBar; - hasScrollX = content.width + (hasScrollY ? scrollbarSize : 0) > container.width; + let viewportOuterSize: Size; + let viewportInnerSize: Size; + let hasScrollX = false; + let hasScrollY = false; + + if (params.autoHeight) { + hasScrollY = false; + hasScrollX = Math.round(columnsTotalWidth) > Math.round(rootSize.width); + + viewportOuterSize = { + width: rootSize.width, + height: topContainerHeight + bottomContainerHeight + contentSize.height, + }; + viewportInnerSize = { + width: Math.max(0, viewportOuterSize.width - (hasScrollY ? scrollbarSize : 0)), + height: Math.max(0, viewportOuterSize.height - (hasScrollX ? scrollbarSize : 0)), + }; + } else { + viewportOuterSize = { + width: rootSize.width, + height: rootSize.height, + }; + viewportInnerSize = { + width: Math.max(0, viewportOuterSize.width), + height: Math.max( + 0, + viewportOuterSize.height - topContainerHeight - bottomContainerHeight, + ), + }; + + const content = contentSize; + const container = viewportInnerSize; + + const hasScrollXIfNoYScrollBar = content.width > container.width; + const hasScrollYIfNoXScrollBar = content.height > container.height; + + if (hasScrollXIfNoYScrollBar || hasScrollYIfNoXScrollBar) { + hasScrollY = hasScrollYIfNoXScrollBar; + hasScrollX = content.width + (hasScrollY ? scrollbarSize : 0) > container.width; + + // We recalculate the scroll y to consider the size of the x scrollbar. + if (hasScrollX) { + hasScrollY = content.height + scrollbarSize > container.height; + } + } - // We recalculate the scroll y to consider the size of the x scrollbar. + if (hasScrollY) { + viewportInnerSize.width -= scrollbarSize; + } if (hasScrollX) { - hasScrollY = content.height + scrollbarSize > container.height; + viewportInnerSize.height -= scrollbarSize; } } - if (hasScrollY) { - viewportInnerSize.width -= scrollbarSize; + if (params.disableHorizontalScroll) { + hasScrollX = false; } - if (hasScrollX) { - viewportInnerSize.height -= scrollbarSize; + + if (params.disableVerticalScroll) { + hasScrollY = false; } - } - if (params.disableHorizontalScroll) { - hasScrollX = false; - } + const rowWidth = Math.max( + viewportOuterSize.width, + columnsTotalWidth + (hasScrollY ? scrollbarSize : 0), + ); - if (params.disableVerticalScroll) { - hasScrollY = false; - } + const minimumSize = { + width: columnsTotalWidth, + height: topContainerHeight + contentSize.height + bottomContainerHeight, + }; - const rowWidth = Math.max( - viewportOuterSize.width, - columnsTotalWidth + (hasScrollY ? scrollbarSize : 0), - ); + const newDimensions: DimensionsState = { + isReady: true, + root: rootSize, + viewportOuterSize, + viewportInnerSize, + contentSize, + minimumSize, + hasScrollX, + hasScrollY, + scrollbarSize, + rowWidth, + rowHeight, + columnsTotalWidth, + leftPinnedWidth, + rightPinnedWidth, + topContainerHeight, + bottomContainerHeight, + }; - const minimumSize = { - width: columnsTotalWidth, - height: topContainerHeight + contentSize.height + bottomContainerHeight, - }; + const prevDimensions = store.state.dimensions; - const newDimensions: DimensionsState = { - isReady: true, - root: rootSize, - viewportOuterSize, - viewportInnerSize, - contentSize, - minimumSize, - hasScrollX, - hasScrollY, - scrollbarSize, - rowWidth, + if (isDeepEqual(prevDimensions as any, newDimensions)) { + return; + } + + store.update({ dimensions: newDimensions }); + onResize?.(newDimensions.root); + }, + [ + store, + refs.container, + params.dimensions.scrollbarSize, + params.autoHeight, + params.disableHorizontalScroll, + params.disableVerticalScroll, + onResize, rowHeight, columnsTotalWidth, leftPinnedWidth, rightPinnedWidth, - topContainerHeight, - bottomContainerHeight, - }; - - const prevDimensions = store.state.dimensions; - - if (isDeepEqual(prevDimensions as any, newDimensions)) { - return; - } - - store.update({ dimensions: newDimensions }); - onResize?.(newDimensions.root); - }, [ - store, - containerNode, - params.dimensions.scrollbarSize, - params.autoHeight, - params.disableHorizontalScroll, - params.disableVerticalScroll, - onResize, - rowHeight, - columnsTotalWidth, - leftPinnedWidth, - rightPinnedWidth, - topPinnedHeight, - bottomPinnedHeight, - ]); + topPinnedHeight, + bottomPinnedHeight, + ], + ); const { resizeThrottleMs } = params; const updateDimensionCallback = useEventCallback(updateDimensions); @@ -265,23 +273,6 @@ function useDimensions(store: Store, params: VirtualizerParams, _api: ); React.useEffect(() => debouncedUpdateDimensions?.clear, [debouncedUpdateDimensions]); - const setRootSize = useEventCallback((rootSize: Size) => { - store.state.rootSize = rootSize; - - if (isFirstSizing.current || !debouncedUpdateDimensions) { - // We want to initialize the grid dimensions as soon as possible to avoid flickering - isFirstSizing.current = false; - updateDimensions(); - } else { - debouncedUpdateDimensions(); - } - }); - - useLayoutEffect( - () => observeRootNode(containerNode, store, setRootSize), - [containerNode, store, setRootSize], - ); - useLayoutEffect(updateDimensions, [updateDimensions]); const rowsMeta = useRowsMeta(store, params, updateDimensions); @@ -429,7 +420,6 @@ function useRowsMeta( }; store.set('rowsMeta', rowsMeta); - if (didHeightsChange) { updateDimensions(); } @@ -528,7 +518,7 @@ function useRowsMeta( }; } -function observeRootNode( +export function observeRootNode( node: Element | null, store: Store, setRootSize: (size: Size) => void, diff --git a/packages/x-virtualizer/src/features/virtualization.ts b/packages/x-virtualizer/src/features/virtualization.ts index 98c0c4bc77612..9bd685f264c12 100644 --- a/packages/x-virtualizer/src/features/virtualization.ts +++ b/packages/x-virtualizer/src/features/virtualization.ts @@ -8,11 +8,10 @@ import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import type { integer } from '@mui/x-internals/types'; import * as platform from '@mui/x-internals/platform'; import { useRunOnce } from '@mui/x-internals/useRunOnce'; -import { useFirstRender } from '@mui/x-internals/useFirstRender'; import { createSelector, useStore, useStoreEffect, Store } from '@mui/x-internals/store'; -import { PinnedRows, PinnedColumns } from '../models/core'; +import { PinnedRows, PinnedColumns, Size } from '../models/core'; import type { CellColSpanInfo } from '../models/colspan'; -import { Dimensions } from './dimensions'; +import { Dimensions, observeRootNode } from './dimensions'; import type { BaseState, VirtualizerParams } from '../useVirtualizer'; import { PinnedRowPosition, @@ -61,6 +60,7 @@ export const EMPTY_RENDER_CONTEXT = { }; const selectors = { + store: createSelector((state: BaseState) => state.virtualization), renderContext: createSelector((state: BaseState) => state.virtualization.renderContext), enabledForRows: createSelector((state: BaseState) => state.virtualization.enabledForRows), enabledForColumns: createSelector((state: BaseState) => state.virtualization.enabledForColumns), @@ -139,7 +139,7 @@ function useVirtualization(store: Store, params: VirtualizerParams, a const hasBottomPinnedRows = pinnedRows.bottom.length > 0; const [panels, setPanels] = React.useState(EMPTY_DETAIL_PANELS); - const [, setRefTick] = React.useState(0); + const isUpdateScheduled = React.useRef(false); const isRenderContextReady = React.useRef(false); @@ -291,7 +291,7 @@ function useVirtualization(store: Store, params: VirtualizerParams, a return nextRenderContext; }); - const forceUpdateRenderContext = useEventCallback(() => { + const forceUpdateRenderContext = () => { // skip update if dimensions are not ready and virtualization is enabled if ( !Dimensions.selectors.dimensions(store.state).isReady && @@ -304,8 +304,27 @@ function useVirtualization(store: Store, params: VirtualizerParams, a // Reset the frozen context when the render context changes, see the illustration in https://github.com/mui/mui-x/pull/12353 frozenContext.current = undefined; updateRenderContext(nextRenderContext); + }; + + const forceUpdateRenderContextCallback = useEventCallback(forceUpdateRenderContext); + + useStoreEffect(store, Dimensions.selectors.dimensions, (previous, next) => { + if (next.isReady) { + forceUpdateRenderContext(); + } }); + useEnhancedEffect(() => { + if (isUpdateScheduled.current) { + forceUpdateRenderContext(); + isUpdateScheduled.current = false; + } + }); + + const scheduleUpdateRenderContext = () => { + isUpdateScheduled.current = true; + }; + const handleScroll = useEventCallback(() => { if (ignoreNextScrollEvent.current) { ignoreNextScrollEvent.current = false; @@ -543,8 +562,8 @@ function useVirtualization(store: Store, params: VirtualizerParams, a if (!isRenderContextReady.current) { return; } - forceUpdateRenderContext(); - }, [enabledForColumns, enabledForRows, forceUpdateRenderContext]); + forceUpdateRenderContextCallback(); + }, [enabledForColumns, enabledForRows, forceUpdateRenderContextCallback]); useEnhancedEffect(() => { if (refs.scroller.current) { @@ -606,19 +625,46 @@ function useVirtualization(store: Store, params: VirtualizerParams, a useStoreEffect(store, Dimensions.selectors.dimensions, forceUpdateRenderContext); - const refSetter = (name: keyof typeof refs) => (node: HTMLDivElement | null) => { - if (node && refs[name].current !== node) { - refs[name].current = node; - setRefTick((tick) => tick + 1); + const refSetter = React.useCallback( + (name: keyof typeof refs) => (node: HTMLDivElement | null) => { + if (node && refs[name].current !== node) { + refs[name].current = node; + } + }, + [refs], + ); + + const isFirstSizing = React.useRef(true); + + const cleanup = React.useRef<() => void | undefined>(undefined); + const cleanupFn = cleanup.current; + React.useEffect(() => { + return cleanupFn; + }, [cleanupFn]); + + const containerRef = useEventCallback((node: HTMLDivElement | null) => { + if (node && refs.container.current !== node) { + refs.container.current = node; + cleanup.current = observeRootNode(node, store, (rootSize: Size) => { + store.state.rootSize = rootSize; + if (isFirstSizing.current || !api.debouncedUpdateDimensions) { + // We want to initialize the grid dimensions as soon as possible to avoid flickering + api.updateDimensions(isFirstSizing.current); + isFirstSizing.current = false; + } else { + api.debouncedUpdateDimensions(); + } + }); } - }; + }); const getters = { setPanels, getOffsetTop, getRows, + rows: params.rows, getContainerProps: () => ({ - ref: refSetter('container'), + ref: containerRef, }), getScrollerProps: () => ({ ref: refSetter('scroller'), @@ -649,17 +695,6 @@ function useVirtualization(store: Store, params: VirtualizerParams, a }), }; - useFirstRender(() => { - store.state = { - ...store.state, - getters, - }; - }); - React.useEffect(() => { - store.update({ getters }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, Object.values(getters)); - /* Placeholder API functions for colspan & rowspan to re-implement */ const getCellColSpanInfo: AbstractAPI['getCellColSpanInfo'] = () => { @@ -679,6 +714,7 @@ function useVirtualization(store: Store, params: VirtualizerParams, a useVirtualization: () => useStore(store, (state) => state), setPanels, forceUpdateRenderContext, + scheduleUpdateRenderContext, getCellColSpanInfo, calculateColSpan, getHiddenCellsOrigin,