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}
/>