diff --git a/docs/src/pages/components/data-grid/editing/FullFeaturedCrudGrid.js b/docs/src/pages/components/data-grid/editing/FullFeaturedCrudGrid.js index 97d5c8bd2874..3bf8ba4ac256 100644 --- a/docs/src/pages/components/data-grid/editing/FullFeaturedCrudGrid.js +++ b/docs/src/pages/components/data-grid/editing/FullFeaturedCrudGrid.js @@ -87,7 +87,7 @@ function EditToolbar(props) { }); apiRef.current.setCellFocus(id, 'name'); - }, 150); + }); }; return ( diff --git a/docs/src/pages/components/data-grid/editing/FullFeaturedCrudGrid.tsx b/docs/src/pages/components/data-grid/editing/FullFeaturedCrudGrid.tsx index 6feb8df98217..2e065c6ff00f 100644 --- a/docs/src/pages/components/data-grid/editing/FullFeaturedCrudGrid.tsx +++ b/docs/src/pages/components/data-grid/editing/FullFeaturedCrudGrid.tsx @@ -94,7 +94,7 @@ function EditToolbar(props: EditToolbarProps) { rowIndex: apiRef.current.getRowsCount() - 1, }); apiRef.current.setCellFocus(id, 'name'); - }, 150); + }); }; return ( diff --git a/docs/src/pages/components/data-grid/rows/ThrottledRowsGrid.js b/docs/src/pages/components/data-grid/rows/ThrottledRowsGrid.js new file mode 100644 index 000000000000..2cd18fcba897 --- /dev/null +++ b/docs/src/pages/components/data-grid/rows/ThrottledRowsGrid.js @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; +import { interval } from 'rxjs'; +import { randomInt, randomUserName } from '@mui/x-data-grid-generator'; + +const columns = [ + { field: 'id' }, + { field: 'username', width: 150 }, + { field: 'age', width: 80, type: 'number' }, +]; + +const rows = [ + { id: 1, username: randomUserName(), age: randomInt(10, 80) }, + { id: 2, username: randomUserName(), age: randomInt(10, 80) }, + { id: 3, username: randomUserName(), age: randomInt(10, 80) }, + { id: 4, username: randomUserName(), age: randomInt(10, 80) }, +]; + +export default function ThrottledRowsGrid() { + const apiRef = useGridApiRef(); + + React.useEffect(() => { + const subscription = interval(10).subscribe(() => { + apiRef.current.updateRows([ + { + id: randomInt(1, 4), + username: randomUserName(), + age: randomInt(10, 80), + }, + { + id: randomInt(1, 4), + username: randomUserName(), + age: randomInt(10, 80), + }, + ]); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [apiRef]); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/rows/ThrottledRowsGrid.tsx b/docs/src/pages/components/data-grid/rows/ThrottledRowsGrid.tsx new file mode 100644 index 000000000000..2cd18fcba897 --- /dev/null +++ b/docs/src/pages/components/data-grid/rows/ThrottledRowsGrid.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; +import { interval } from 'rxjs'; +import { randomInt, randomUserName } from '@mui/x-data-grid-generator'; + +const columns = [ + { field: 'id' }, + { field: 'username', width: 150 }, + { field: 'age', width: 80, type: 'number' }, +]; + +const rows = [ + { id: 1, username: randomUserName(), age: randomInt(10, 80) }, + { id: 2, username: randomUserName(), age: randomInt(10, 80) }, + { id: 3, username: randomUserName(), age: randomInt(10, 80) }, + { id: 4, username: randomUserName(), age: randomInt(10, 80) }, +]; + +export default function ThrottledRowsGrid() { + const apiRef = useGridApiRef(); + + React.useEffect(() => { + const subscription = interval(10).subscribe(() => { + apiRef.current.updateRows([ + { + id: randomInt(1, 4), + username: randomUserName(), + age: randomInt(10, 80), + }, + { + id: randomInt(1, 4), + username: randomUserName(), + age: randomInt(10, 80), + }, + ]); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [apiRef]); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/rows/rows.md b/docs/src/pages/components/data-grid/rows/rows.md index 9a6e4266cd7b..4de004b91130 100644 --- a/docs/src/pages/components/data-grid/rows/rows.md +++ b/docs/src/pages/components/data-grid/rows/rows.md @@ -53,6 +53,16 @@ Alternatively, if you would like to delete a row, you would need to pass an extr apiRef.current.updateRows([{ id: 1, _action: 'delete' }]); ``` +### High frequency [](https://material-ui.com/store/items/material-ui-pro/) + +Whenever the rows are updated, the grid has to apply the sorting and filters. This can be a problem if you have high frequency updates. To maintain good performances, the grid allows to batch the updates and only apply them after a period of time. The `throttleRowsMs` prop can be used to define the frequency (in milliseconds) at which rows updates are applied. + +When receiving updates more frequently than this threshold, the grid will wait before updating the rows. + +The following demo updates the rows every 10ms, but they are only applied every 2 seconds. + +{{"demo": "pages/components/data-grid/rows/ThrottledRowsGrid.js", "bg": "inline"}} + ## Row height By default, the rows have a height of 52 pixels. diff --git a/packages/grid/_modules_/grid/constants/eventsConstants.ts b/packages/grid/_modules_/grid/constants/eventsConstants.ts index 21f3ec5907ad..2eaaa928d9f0 100644 --- a/packages/grid/_modules_/grid/constants/eventsConstants.ts +++ b/packages/grid/_modules_/grid/constants/eventsConstants.ts @@ -281,23 +281,11 @@ export enum GridEvents { * Fired when the user ends reordering a column. */ columnOrderChange = 'columnOrderChange', - /** - * Fired when some of the rows are updated. - * @ignore - do not document. - */ - rowsUpdate = 'rowsUpdate', /** * Fired when all the rows are updated. * @ignore - do not document. */ rowsSet = 'rowsSet', - /** - * Implementation detail. - * Fired to reset the sortedRow when the set of rows changes. - * It's important as the rendered rows are coming from the sortedRow - * @ignore - do not document. - */ - rowsClear = 'rowsClear', /** * Fired when the columns state is changed. * Called with an array of strings corresponding to the field names. diff --git a/packages/grid/_modules_/grid/hooks/features/core/gridState.ts b/packages/grid/_modules_/grid/hooks/features/core/gridState.ts index 2d9752e9ebe2..66ed4a5daed5 100644 --- a/packages/grid/_modules_/grid/hooks/features/core/gridState.ts +++ b/packages/grid/_modules_/grid/hooks/features/core/gridState.ts @@ -23,7 +23,7 @@ import { } from '../filter/visibleGridRowsState'; import { GridFocusState, GridTabIndexState } from '../focus/gridFocusState'; import { GridPreferencePanelState } from '../preferencesPanel/gridPreferencePanelState'; -import { getInitialGridRowState, InternalGridRowsState } from '../rows/gridRowsState'; +import { getInitialGridRowState, GridRowsState } from '../rows/gridRowsState'; import { GridSelectionModel } from '../../../models/gridSelectionModel'; import { getInitialGridSortingState, GridSortingState } from '../sorting/gridSortingState'; import { @@ -33,7 +33,7 @@ import { import { getInitialPaginationState, GridPaginationState } from '../pagination/gridPaginationState'; export interface GridState { - rows: InternalGridRowsState; + rows: GridRowsState; editRows: GridEditRowsModel; pagination: GridPaginationState; columns: GridColumnsState; diff --git a/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts b/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts index 80eff143d4fe..9d128695f15e 100644 --- a/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts +++ b/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts @@ -321,6 +321,5 @@ export const useGridFilter = ( }, [apiRef, logger, props.filterModel, setGridState]); useGridApiEventHandler(apiRef, GridEvents.rowsSet, apiRef.current.applyFilters); - useGridApiEventHandler(apiRef, GridEvents.rowsUpdate, apiRef.current.applyFilters); useGridApiEventHandler(apiRef, GridEvents.columnsChange, onColUpdated); }; diff --git a/packages/grid/_modules_/grid/hooks/features/rows/gridRowsSelector.ts b/packages/grid/_modules_/grid/hooks/features/rows/gridRowsSelector.ts index 56f0aac7d48c..6f8ca6dc17bd 100644 --- a/packages/grid/_modules_/grid/hooks/features/rows/gridRowsSelector.ts +++ b/packages/grid/_modules_/grid/hooks/features/rows/gridRowsSelector.ts @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; import { GridRowId, GridRowModel } from '../../../models/gridRows'; import { GridState } from '../core/gridState'; -import { InternalGridRowsState } from './gridRowsState'; +import { GridRowsState } from './gridRowsState'; export type GridRowsLookup = Record; @@ -9,20 +9,20 @@ export const gridRowsStateSelector = (state: GridState) => state.rows; export const gridRowCountSelector = createSelector( gridRowsStateSelector, - (rows: InternalGridRowsState) => rows && rows.totalRowCount, + (rows: GridRowsState) => rows.totalRowCount, ); export const gridRowsLookupSelector = createSelector( gridRowsStateSelector, - (rows: InternalGridRowsState) => rows && rows.idRowsLookup, + (rows: GridRowsState) => rows.idRowsLookup, ); export const unorderedGridRowIdsSelector = createSelector( gridRowsStateSelector, - (rows: InternalGridRowsState) => rows.allRows, + (rows: GridRowsState) => rows.allRows, ); export const unorderedGridRowModelsSelector = createSelector( gridRowsStateSelector, - (rows: InternalGridRowsState) => rows.allRows.map((id) => rows.idRowsLookup[id]), + (rows: GridRowsState) => rows.allRows.map((id) => rows.idRowsLookup[id]), ); diff --git a/packages/grid/_modules_/grid/hooks/features/rows/gridRowsState.ts b/packages/grid/_modules_/grid/hooks/features/rows/gridRowsState.ts index 2509273373db..f86d234f2f04 100644 --- a/packages/grid/_modules_/grid/hooks/features/rows/gridRowsState.ts +++ b/packages/grid/_modules_/grid/hooks/features/rows/gridRowsState.ts @@ -1,12 +1,12 @@ import { GridRowId, GridRowModel } from '../../../models/gridRows'; -export interface InternalGridRowsState { +export interface GridRowsState { idRowsLookup: Record; allRows: GridRowId[]; totalRowCount: number; } -export const getInitialGridRowState: () => InternalGridRowsState = () => ({ +export const getInitialGridRowState: () => GridRowsState = () => ({ idRowsLookup: {}, allRows: [], totalRowCount: 0, diff --git a/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts b/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts index d8cb554911f9..25eaa74ee39d 100644 --- a/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts +++ b/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts @@ -6,7 +6,6 @@ import { GridRowApi } from '../../../models/api/gridRowApi'; import { checkGridRowIdIsValid, GridRowModel, - GridRowModelUpdate, GridRowId, GridRowsProp, GridRowIdGetter, @@ -15,9 +14,18 @@ import { import { useGridApiMethod } from '../../root/useGridApiMethod'; import { useGridLogger } from '../../utils/useGridLogger'; import { useGridState } from '../core/useGridState'; -import { getInitialGridRowState, InternalGridRowsState } from './gridRowsState'; -import { useGridSelector } from '../core/useGridSelector'; -import { gridRowsStateSelector } from './gridRowsSelector'; +import { getInitialGridRowState, GridRowsState } from './gridRowsState'; +import { + gridRowCountSelector, + gridRowsLookupSelector, + unorderedGridRowIdsSelector, +} from './gridRowsSelector'; + +export interface GridRowsInternalCache { + state: GridRowsState; + timeout: NodeJS.Timeout | null; + lastUpdateMs: number | null; +} function getGridRowId( rowData: GridRowData, @@ -31,12 +39,12 @@ function getGridRowId( export function convertGridRowsPropToState( rows: GridRowsProp, - totalRowCount?: number, + propRowCount?: number, rowIdGetter?: GridRowIdGetter, -): InternalGridRowsState { - const state: InternalGridRowsState = { +): GridRowsState { + const state: GridRowsState = { ...getInitialGridRowState(), - totalRowCount: totalRowCount && totalRowCount > rows.length ? totalRowCount : rows.length, + totalRowCount: propRowCount && propRowCount > rows.length ? propRowCount : rows.length, }; rows.forEach((rowData) => { @@ -54,33 +62,19 @@ export function convertGridRowsPropToState( */ export const useGridRows = ( apiRef: GridApiRef, - props: Pick, + props: Pick, ): void => { const logger = useGridLogger(apiRef, 'useGridRows'); const [, setGridState, forceUpdate] = useGridState(apiRef); - const stateRows = useGridSelector(apiRef, gridRowsStateSelector); - const updateTimeout = React.useRef(); - - const delayedForceUpdate = React.useCallback( - (preUpdateCallback?: Function) => { - if (updateTimeout.current == null) { - updateTimeout.current = setTimeout(() => { - logger.debug(`Updating component`); - updateTimeout.current = null; - if (preUpdateCallback) { - preUpdateCallback(); - } - forceUpdate(); - }, 100); - } - }, - [logger, forceUpdate], - ); - const internalRowsState = React.useRef(stateRows); + const rowsCache = React.useRef({ + state: getInitialGridRowState(), + timeout: null, + lastUpdateMs: null, + }); - const getRowIndexFromId = React.useCallback( - (id: GridRowId): number => { + const getRowIndex = React.useCallback( + (id) => { if (apiRef.current.getSortedRowIds) { return apiRef.current.getSortedRowIds().indexOf(id); } @@ -88,8 +82,9 @@ export const useGridRows = ( }, [apiRef], ); - const getRowIdFromRowIndex = React.useCallback( - (index: number): GridRowId => { + + const getRowIdFromRowIndex = React.useCallback( + (index) => { if (apiRef.current.getSortedRowIds) { return apiRef.current.getSortedRowIds()[index]; } @@ -97,125 +92,153 @@ export const useGridRows = ( }, [apiRef], ); - const getRow = React.useCallback( - (id: GridRowId): GridRowModel | null => apiRef.current.state.rows.idRowsLookup[id] ?? null, + + const getRow = React.useCallback( + (id) => gridRowsLookupSelector(apiRef.current.state)[id] ?? null, [apiRef], ); - const setRowsState = React.useCallback( - ( - rows: GridRowModel[] | readonly GridRowModel[], - rowCount: GridComponentProps['rowCount'], - getRowId: GridComponentProps['getRowId'], - waitBeforeUpdate: boolean, - ) => { - logger.debug(`updating all rows, new length ${rows.length}`); - - if (internalRowsState.current.allRows.length > 0) { - apiRef.current.publishEvent(GridEvents.rowsClear); + const throttledRowsChange = React.useCallback( + (newState: GridRowsState, throttle: boolean) => { + const run = () => { + rowsCache.current.timeout = null; + rowsCache.current.lastUpdateMs = Date.now(); + setGridState((state) => ({ ...state, rows: rowsCache.current.state })); + apiRef.current.publishEvent(GridEvents.rowsSet); + forceUpdate(); + }; + + if (rowsCache.current.timeout) { + clearTimeout(rowsCache.current.timeout); } - internalRowsState.current = convertGridRowsPropToState(rows, rowCount, getRowId); + rowsCache.current.state = newState; + rowsCache.current.timeout = null; - setGridState((state) => ({ ...state, rows: internalRowsState.current })); + if (!throttle) { + run(); + return; + } - if (waitBeforeUpdate) { - delayedForceUpdate(() => apiRef.current.publishEvent(GridEvents.rowsSet)); - } else { - forceUpdate(); - apiRef.current.publishEvent(GridEvents.rowsSet); + const throttleRemainingTimeMs = + rowsCache.current.lastUpdateMs === null + ? 0 + : props.throttleRowsMs - (Date.now() - rowsCache.current.lastUpdateMs); + + if (throttleRemainingTimeMs > 0) { + rowsCache.current.timeout = setTimeout(run, throttleRemainingTimeMs); + return; } + + run(); }, - [apiRef, logger, setGridState, forceUpdate, delayedForceUpdate], + [apiRef, forceUpdate, setGridState, rowsCache, props.throttleRowsMs], ); const setRows = React.useCallback( - (rows) => setRowsState(rows, props.rowCount, props.getRowId, true), - [setRowsState, props.rowCount, props.getRowId], + (rows) => { + logger.debug(`Updating all rows, new length ${rows.length}`); + throttledRowsChange(convertGridRowsPropToState(rows, props.rowCount, props.getRowId), true); + }, + [logger, throttledRowsChange, props.rowCount, props.getRowId], ); - const updateRows = React.useCallback( - (updates: GridRowModelUpdate[]) => { + const updateRows = React.useCallback( + (updates) => { // we removes duplicate updates. A server can batch updates, and send several updates for the same row in one fn call. - const uniqUpdates = updates.reduce((acc, update) => { + const uniqUpdates = new Map(); + + updates.forEach((update) => { const id = getGridRowId( update, props.getRowId, 'A row was provided without id when calling updateRows():', ); - acc[id] = acc[id] != null ? { ...acc[id!], ...update } : update; - return acc; - }, {} as { [id: string]: GridRowModel }); - const addedRows: GridRowModel[] = []; + if (uniqUpdates.has(id)) { + uniqUpdates.set(id, { ...uniqUpdates.get(id), ...update }); + } else { + uniqUpdates.set(id, update); + } + }); + const deletedRowIds: GridRowId[] = []; - let updatedLookup: null | {} = null; - Object.entries(uniqUpdates).forEach(([id, partialRow]) => { + const idRowsLookup = { ...rowsCache.current.state.idRowsLookup }; + let allRows = [...rowsCache.current.state.allRows]; + + uniqUpdates.forEach((partialRow, id) => { // eslint-disable-next-line no-underscore-dangle if (partialRow._action === 'delete') { + delete idRowsLookup[id]; deletedRowIds.push(id); return; } - const oldRow = getRow(id); + const oldRow = apiRef.current.getRow(id); if (!oldRow) { - addedRows.push(partialRow); + idRowsLookup[id] = partialRow; + allRows.push(id); return; } - if (!updatedLookup) { - updatedLookup = { ...internalRowsState.current.idRowsLookup }; - } - - updatedLookup[id] = { - ...oldRow, - ...partialRow, - }; + idRowsLookup[id] = { ...apiRef.current.getRow(id), ...partialRow }; }); - if (updatedLookup) { - internalRowsState.current.idRowsLookup = updatedLookup; - setGridState((state) => ({ ...state, rows: { ...internalRowsState.current } })); - } - if (deletedRowIds.length > 0 || addedRows.length > 0) { - deletedRowIds.forEach((id) => { - delete internalRowsState.current.idRowsLookup[id]; - }); - const newRows = [ - ...Object.values(internalRowsState.current.idRowsLookup), - ...addedRows, - ]; - setRows(newRows); + if (deletedRowIds.length > 0) { + allRows = allRows.filter((id) => !deletedRowIds.includes(id)); } - delayedForceUpdate(() => apiRef.current.publishEvent(GridEvents.rowsUpdate)); + + const totalRowCount = + props.rowCount && props.rowCount > allRows.length ? props.rowCount : allRows.length; + + const state: GridRowsState = { + idRowsLookup, + allRows, + totalRowCount, + }; + + throttledRowsChange(state, true); }, - [apiRef, delayedForceUpdate, getRow, props.getRowId, setGridState, setRows], + [apiRef, props.getRowId, props.rowCount, throttledRowsChange], + ); + + const getRowModels = React.useCallback(() => { + const allRows = unorderedGridRowIdsSelector(apiRef.current.state); + const idRowsLookup = gridRowsLookupSelector(apiRef.current.state); + + return new Map(allRows.map((id) => [id, idRowsLookup[id]])); + }, [apiRef]); + + const getRowsCount = React.useCallback( + () => gridRowCountSelector(apiRef.current.state), + [apiRef], ); - const getRowModels = React.useCallback( - () => - new Map( - apiRef.current.state.rows.allRows.map((id) => [ - id, - apiRef.current.state.rows.idRowsLookup[id], - ]), - ), + const getAllRowIds = React.useCallback( + () => unorderedGridRowIdsSelector(apiRef.current.state), [apiRef], ); - const getRowsCount = React.useCallback(() => apiRef.current.state.rows.totalRowCount, [apiRef]); - const getAllRowIds = React.useCallback(() => apiRef.current.state.rows.allRows, [apiRef]); React.useEffect(() => { - return () => clearTimeout(updateTimeout!.current); + return () => { + if (rowsCache.current.timeout !== null) { + // eslint-disable-next-line react-hooks/exhaustive-deps + clearTimeout(rowsCache.current.timeout); + } + }; }, []); React.useEffect(() => { - setRowsState(props.rows, props.rowCount, props.getRowId, false); - }, [setRowsState, props.rows, props.rowCount, props.getRowId]); + logger.debug(`Updating all rows, new length ${props.rows.length}`); + throttledRowsChange( + convertGridRowsPropToState(props.rows, props.rowCount, props.getRowId), + false, + ); + }, [props.rows, props.rowCount, props.getRowId, logger, throttledRowsChange]); const rowApi: GridRowApi = { - getRowIndex: getRowIndexFromId, + getRowIndex, getRowIdFromRowIndex, getRow, getRowModels, @@ -224,5 +247,6 @@ export const useGridRows = ( setRows, updateRows, }; + useGridApiMethod(apiRef, rowApi, 'GridRowApi'); }; diff --git a/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts b/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts index 5ef88aae804f..ee8bec88e0ba 100644 --- a/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts +++ b/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts @@ -279,12 +279,6 @@ export const useGridSorting = ( [sortColumn], ); - const onRowsCleared = React.useCallback(() => { - setGridState((state) => { - return { ...state, sorting: { ...state.sorting, sortedRows: [] } }; - }); - }, [setGridState]); - const onColUpdated = React.useCallback(() => { // When the columns change we check that the sorted columns are still part of the dataset setGridState((state) => { @@ -308,7 +302,5 @@ export const useGridSorting = ( useGridApiEventHandler(apiRef, GridEvents.columnHeaderClick, handleColumnHeaderClick); useGridApiEventHandler(apiRef, GridEvents.columnHeaderKeyDown, handleColumnHeaderKeyDown); useGridApiEventHandler(apiRef, GridEvents.rowsSet, apiRef.current.applySorting); - useGridApiEventHandler(apiRef, GridEvents.rowsClear, onRowsCleared); - useGridApiEventHandler(apiRef, GridEvents.rowsUpdate, apiRef.current.applySorting); useGridApiEventHandler(apiRef, GridEvents.columnsChange, onColUpdated); }; diff --git a/packages/grid/_modules_/grid/models/gridOptions.tsx b/packages/grid/_modules_/grid/models/gridOptions.tsx index 1ce06c342dc1..2711e3cbfa3a 100644 --- a/packages/grid/_modules_/grid/models/gridOptions.tsx +++ b/packages/grid/_modules_/grid/models/gridOptions.tsx @@ -218,6 +218,12 @@ export interface GridSimpleOptions { * @default "client" */ sortingMode: GridFeatureMode; + /** + * If positive, the Grid will throttle updates coming from `apiRef.current.updateRows` and `apiRef.current.setRows`. + * It can be useful if you have a high update rate but do not want to do heavy work like filtering / sorting or rendering on each individual update. + * @default 0 + */ + throttleRowsMs: number; } /** @@ -260,4 +266,5 @@ export const GRID_DEFAULT_SIMPLE_OPTIONS: GridSimpleOptions = { showColumnRightBorder: false, sortingOrder: ['asc' as const, 'desc' as const, null], sortingMode: GridFeatureModeConstant.client, + throttleRowsMs: 0, }; diff --git a/packages/grid/data-grid/src/DataGridProps.ts b/packages/grid/data-grid/src/DataGridProps.ts index d147c8ed206b..dac80ef5b90e 100644 --- a/packages/grid/data-grid/src/DataGridProps.ts +++ b/packages/grid/data-grid/src/DataGridProps.ts @@ -14,6 +14,7 @@ export type DataGridProps = Omit< | 'disableMultipleColumnsFiltering' | 'disableMultipleColumnsSorting' | 'disableMultipleSelection' + | 'throttleRowsMs' | 'hideFooterRowCount' | 'options' | 'onRowsScrollEnd' diff --git a/packages/grid/data-grid/src/tests/rows.DataGrid.test.tsx b/packages/grid/data-grid/src/tests/rows.DataGrid.test.tsx index ae81f0fd7631..18454573afb6 100644 --- a/packages/grid/data-grid/src/tests/rows.DataGrid.test.tsx +++ b/packages/grid/data-grid/src/tests/rows.DataGrid.test.tsx @@ -9,10 +9,11 @@ import { waitFor, } from 'test/utils'; import { expect } from 'chai'; -import { spy, stub } from 'sinon'; +import { spy, stub, SinonFakeTimers, useFakeTimers } from 'sinon'; import Portal from '@mui/material/Portal'; -import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid'; +import { DataGrid, DataGridProps, GridActionsCellItem } from '@mui/x-data-grid'; import { getColumnValues, getRow } from 'test/utils/helperFn'; +import { getData } from 'storybook/src/data/data-service'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); @@ -42,7 +43,7 @@ describe(' - Rows', () => { columns: [{ field: 'clientId' }, { field: 'first' }, { field: 'age' }], }; - describe('getRowId', () => { + describe('props: getRowId', () => { it('should allow to select a field as id', () => { const getRowId = (row) => `${row.clientId}`; render( @@ -54,6 +55,33 @@ describe(' - Rows', () => { }); }); + describe('props: rows', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should support new dataset', () => { + const { rows, columns } = getData(5, 2); + + const Test = (props: Pick) => ( +
+ +
+ ); + + const { setProps } = render(); + expect(getColumnValues(0)).to.deep.equal(['0', '1']); + setProps({ rows }); + expect(getColumnValues(0)).to.deep.equal(['0', '1', '2', '3', '4']); + }); + }); + it('should ignore events coming from a portal in the cell', () => { const handleRowClick = spy(); const InputCell = () => ; diff --git a/packages/grid/data-grid/src/useDataGridProps.ts b/packages/grid/data-grid/src/useDataGridProps.ts index 2ccee8315820..e69059943515 100644 --- a/packages/grid/data-grid/src/useDataGridProps.ts +++ b/packages/grid/data-grid/src/useDataGridProps.ts @@ -16,6 +16,7 @@ const FORCED_PROPS: { [key in ForcedPropsKey]-?: GridInputComponentProps[key] } disableMultipleColumnsFiltering: true, disableMultipleColumnsSorting: true, disableMultipleSelection: true, + throttleRowsMs: undefined, hideFooterRowCount: false, pagination: true, onRowsScrollEnd: undefined, diff --git a/packages/grid/x-grid/src/DataGridPro.tsx b/packages/grid/x-grid/src/DataGridPro.tsx index 09e1f29b0fad..cf0f156d2f6b 100644 --- a/packages/grid/x-grid/src/DataGridPro.tsx +++ b/packages/grid/x-grid/src/DataGridPro.tsx @@ -705,4 +705,10 @@ DataGridProRaw.propTypes = { * @ignore */ style: PropTypes.object, + /** + * If positive, the Grid will throttle updates coming from `apiRef.current.updateRows` and `apiRef.current.setRows`. + * It can be useful if you have a high update rate but do not want to do heavy work like filtering / sorting or rendering on each individual update. + * @default 0 + */ + throttleRowsMs: PropTypes.number, } as any; diff --git a/packages/grid/x-grid/src/tests/editRows.DataGridPro.test.tsx b/packages/grid/x-grid/src/tests/editRows.DataGridPro.test.tsx index 2f06b832bf26..685b3c87d585 100644 --- a/packages/grid/x-grid/src/tests/editRows.DataGridPro.test.tsx +++ b/packages/grid/x-grid/src/tests/editRows.DataGridPro.test.tsx @@ -51,6 +51,7 @@ describe(' - Edit Rows', () => { { field: 'brand', editable: true }, { field: 'year', editable: true }, ], + throttleRowsMs: 0, }; }); diff --git a/packages/grid/x-grid/src/tests/filtering.DataGridPro.test.tsx b/packages/grid/x-grid/src/tests/filtering.DataGridPro.test.tsx index 97fec65e2714..ca2a06b1f992 100644 --- a/packages/grid/x-grid/src/tests/filtering.DataGridPro.test.tsx +++ b/packages/grid/x-grid/src/tests/filtering.DataGridPro.test.tsx @@ -112,7 +112,6 @@ describe(' - Filter', () => { }, ]; apiRef.current.setRows(newRows); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Asics']); }); @@ -120,7 +119,6 @@ describe(' - Filter', () => { render(); apiRef.current.updateRows([{ id: 1, brand: 'Fila' }]); apiRef.current.updateRows([{ id: 0, brand: 'Patagonia' }]); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Patagonia', 'Fila', 'Puma']); }); diff --git a/packages/grid/x-grid/src/tests/rows.DataGridPro.test.tsx b/packages/grid/x-grid/src/tests/rows.DataGridPro.test.tsx index 4e7785d41e75..dc398913343e 100644 --- a/packages/grid/x-grid/src/tests/rows.DataGridPro.test.tsx +++ b/packages/grid/x-grid/src/tests/rows.DataGridPro.test.tsx @@ -16,6 +16,8 @@ import { DataGridProProps, } from '@mui/x-data-grid-pro'; import { useData } from 'packages/storybook/src/hooks/useData'; +import { DataGridProps } from '@mui/x-data-grid'; +import { getData } from 'storybook/src/data/data-service'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); @@ -86,8 +88,6 @@ describe(' - Rows', () => { { clientId: 'c2', age: 30 }, { clientId: 'c3', age: 31 }, ]); - clock.tick(100); - expect(getColumnValues(2)).to.deep.equal(['11', '30', '31']); }); }); @@ -136,7 +136,25 @@ describe(' - Rows', () => { }); }); - describe('updateRows', () => { + describe('props: rows', () => { + it('should not throttle even when props.throttleRowsMs is defined', () => { + const { rows, columns } = getData(5, 2); + + const Test = (props: Pick) => ( +
+ +
+ ); + + const { setProps } = render(); + + expect(getColumnValues(0)).to.deep.equal(['0', '1']); + setProps({ rows }); + expect(getColumnValues(0)).to.deep.equal(['0', '1', '2', '3', '4']); + }); + }); + + describe('apiRef: updateRows', () => { beforeEach(() => { clock = useFakeTimers(); @@ -175,29 +193,21 @@ describe(' - Rows', () => { ); }; - it('should allow to reset rows with setRows and render after 100ms', () => { + it('should not throttle by default', () => { render(); expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); - const newRows = [ - { - id: 3, - brand: 'Asics', - }, - ]; - apiRef.current.setRows(newRows); + apiRef.current.updateRows([{ id: 1, brand: 'Fila' }]); + expect(getColumnValues()).to.deep.equal(['Nike', 'Fila', 'Puma']); + }); - clock.tick(50); + it('should allow to enable throttle', () => { + render(); expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); + apiRef.current.updateRows([{ id: 1, brand: 'Fila' }]); clock.tick(50); - expect(getColumnValues()).to.deep.equal(['Asics']); - - apiRef.current.setRows(baselineProps.rows); - // Force an update before the 100ms - apiRef.current.forceUpdate(() => apiRef.current.state); - // Tradeoff, the value is YOLO - expect(getColumnValues()).to.deep.equal(['Nike']); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); + clock.tick(50); + expect(getColumnValues()).to.deep.equal(['Nike', 'Fila', 'Puma']); }); it('should allow to update row data', () => { @@ -205,7 +215,6 @@ describe(' - Rows', () => { apiRef.current.updateRows([{ id: 1, brand: 'Fila' }]); apiRef.current.updateRows([{ id: 0, brand: 'Pata' }]); apiRef.current.updateRows([{ id: 2, brand: 'Pum' }]); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Pata', 'Fila', 'Pum']); }); @@ -215,7 +224,6 @@ describe(' - Rows', () => { apiRef.current.updateRows([{ id: 0, brand: 'Pata' }]); apiRef.current.updateRows([{ id: 2, brand: 'Pum' }]); apiRef.current.updateRows([{ id: 3, brand: 'Jordan' }]); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Pata', 'Fila', 'Pum', 'Jordan']); }); @@ -227,7 +235,6 @@ describe(' - Rows', () => { { id: 2, brand: 'Pum' }, { id: 3, brand: 'Jordan' }, ]); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Pata', 'Fila', 'Pum', 'Jordan']); }); @@ -237,7 +244,6 @@ describe(' - Rows', () => { apiRef.current.updateRows([{ id: 0, brand: 'Apple' }]); apiRef.current.updateRows([{ id: 2, _action: 'delete' }]); apiRef.current.updateRows([{ id: 5, brand: 'Atari' }]); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Apple', 'Atari']); }); @@ -249,7 +255,6 @@ describe(' - Rows', () => { { id: 2, _action: 'delete' }, { id: 5, brand: 'Atari' }, ]); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Apple', 'Atari']); }); @@ -277,11 +282,81 @@ describe(' - Rows', () => { { idField: 2, _action: 'delete' }, { idField: 5, brand: 'Atari' }, ]); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Apple', 'Atari']); }); }); + describe('apiRef: setRows', () => { + beforeEach(() => { + clock = useFakeTimers(); + + baselineProps = { + autoHeight: isJSDOM, + rows: [ + { + id: 0, + brand: 'Nike', + }, + { + id: 1, + brand: 'Adidas', + }, + { + id: 2, + brand: 'Puma', + }, + ], + columns: [{ field: 'brand', headerName: 'Brand' }], + }; + }); + + afterEach(() => { + clock.restore(); + }); + + let apiRef: GridApiRef; + + const TestCase = (props: Partial) => { + apiRef = useGridApiRef(); + return ( +
+ +
+ ); + }; + + it('should not throttle by default', () => { + render(); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); + const newRows = [ + { + id: 3, + brand: 'Asics', + }, + ]; + apiRef.current.setRows(newRows); + + expect(getColumnValues()).to.deep.equal(['Asics']); + }); + + it('should allow to enable throttle', () => { + render(); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); + const newRows = [ + { + id: 3, + brand: 'Asics', + }, + ]; + apiRef.current.setRows(newRows); + + clock.tick(50); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); + clock.tick(50); + expect(getColumnValues()).to.deep.equal(['Asics']); + }); + }); + describe('virtualization', () => { before(function beforeHook() { if (isJSDOM) { diff --git a/packages/grid/x-grid/src/tests/sorting.DataGridPro.test.tsx b/packages/grid/x-grid/src/tests/sorting.DataGridPro.test.tsx index aed403dc5811..3fb6d3823962 100644 --- a/packages/grid/x-grid/src/tests/sorting.DataGridPro.test.tsx +++ b/packages/grid/x-grid/src/tests/sorting.DataGridPro.test.tsx @@ -7,7 +7,7 @@ import { useGridApiRef, } from '@mui/x-data-grid-pro'; import { expect } from 'chai'; -import { spy, useFakeTimers } from 'sinon'; +import { spy } from 'sinon'; import { getColumnValues, getCell, getColumnHeaderCell } from 'test/utils/helperFn'; import { createClientRenderStrictMode, @@ -23,7 +23,6 @@ import { useData } from 'packages/storybook/src/hooks/useData'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); describe(' - Sorting', () => { - let clock; const baselineProps = { autoHeight: isJSDOM, rows: [ @@ -46,14 +45,6 @@ describe(' - Sorting', () => { columns: [{ field: 'brand' }, { field: 'year', type: 'number' }], }; - beforeEach(() => { - clock = useFakeTimers(); - }); - - afterEach(() => { - clock.restore(); - }); - // TODO v5: replace with createClientRender const render = createClientRenderStrictMode(); @@ -102,7 +93,6 @@ describe(' - Sorting', () => { }, ]; apiRef.current.setRows(newRows); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Asics', 'Hugo', 'RedBull']); }); @@ -110,7 +100,6 @@ describe(' - Sorting', () => { renderBrandSortedAsc(); apiRef.current.updateRows([{ id: 1, brand: 'Fila' }]); apiRef.current.updateRows([{ id: 0, brand: 'Patagonia' }]); - clock.tick(100); expect(getColumnValues()).to.deep.equal(['Fila', 'Patagonia', 'Puma']); }); @@ -191,10 +180,6 @@ describe(' - Sorting', () => { }); describe('performance', () => { - beforeEach(() => { - clock.restore(); - }); - it('should sort 5,000 rows in less than 200 ms', async function test() { // It's simpler to only run the performance test in a single controlled environment. if (!/HeadlessChrome/.test(window.navigator.userAgent)) { diff --git a/packages/storybook/src/components/feed-grid.tsx b/packages/storybook/src/components/feed-grid.tsx index 23c08c71c054..5222c42d3d2e 100644 --- a/packages/storybook/src/components/feed-grid.tsx +++ b/packages/storybook/src/components/feed-grid.tsx @@ -6,11 +6,11 @@ import { pricingColumns, PricingModel } from '../data/streaming/pricing-service' import { subscribeFeed } from '../data/streaming/single-subscription-service'; export interface FeedGridProps extends Omit { - min?: number; - max?: number; + min: number; + max: number; } export const FeedGrid = (props: FeedGridProps) => { - const { min, max } = props; + const { min, max, ...other } = props; const [columns] = React.useState(pricingColumns); const [rows] = React.useState([]); @@ -67,7 +67,7 @@ export const FeedGrid = (props: FeedGridProps) => {
- +
); diff --git a/packages/storybook/src/components/pricing-grid.tsx b/packages/storybook/src/components/pricing-grid.tsx index 529fee609b78..879ee395c955 100644 --- a/packages/storybook/src/components/pricing-grid.tsx +++ b/packages/storybook/src/components/pricing-grid.tsx @@ -10,11 +10,11 @@ import { import { currencyPairs } from '../data/currency-pairs'; export interface PricingGridProps extends Omit { - min?: number; - max?: number; + min: number; + max: number; } export const PricingGrid = (props: PricingGridProps) => { - const { min, max } = props; + const { min, max, ...other } = props; const [columns] = React.useState(pricingColumns); const [rows] = React.useState([]); @@ -54,6 +54,7 @@ export const PricingGrid = (props: PricingGridProps) => { subscribeToStream(); } }; + const getRowId = React.useCallback((row) => row.idfield, []); return ( @@ -68,7 +69,7 @@ export const PricingGrid = (props: PricingGridProps) => {
- +
); diff --git a/packages/storybook/src/stories/grid-streaming.stories.tsx b/packages/storybook/src/stories/grid-streaming.stories.tsx index 76df0dd8a77a..4de77bfa6c0c 100644 --- a/packages/storybook/src/stories/grid-streaming.stories.tsx +++ b/packages/storybook/src/stories/grid-streaming.stories.tsx @@ -27,11 +27,13 @@ export const SlowUpdateGrid = () => {

action('onSelectionChange', { depth: 1 })(params)} + throttleRowsMs={100} {...rate} /> ); }; + export const FastUpdateGrid = () => { const rate = { min: 100, max: 500 }; @@ -42,13 +44,30 @@ export const FastUpdateGrid = () => {

action('onSelectionChange', { depth: 1 })(params)} + throttleRowsMs={100} {...rate} /> ); }; + export const SingleSubscriptionFast = () => { - const rate = { min: 100, max: 500 }; + const rate = { min: 50, max: 500 }; + return ( + +

+ One Subscription for the whole feed! Update rate between {rate.min} - {rate.max} ms! +

+ action('onSelectionChange', { depth: 1 })(params)} + {...rate} + /> +
+ ); +}; + +export const SingleSubscriptionFastWithThrottle = () => { + const rate = { min: 50, max: 500 }; return (

@@ -56,6 +75,7 @@ export const SingleSubscriptionFast = () => {

action('onSelectionChange', { depth: 1 })(params)} + throttleRowsMs={500} {...rate} />