@mui/x-data-grid-pro 8.27.2 → 8.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,95 @@
5
5
  All notable changes to this project will be documented in this file.
6
6
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
7
7
 
8
+ ## 8.27.4
9
+
10
+ _Mar 5, 2026_
11
+
12
+ We'd like to extend a big thank you to the 6 contributors who made this release possible. Here are some highlights ✨:
13
+
14
+ - 🐞 Bugfixes
15
+ - 🌎 Improve Ukrainian (uk-UA) locale
16
+
17
+ Special thanks go out to this community member for their valuable contribution:
18
+ @lion1963
19
+
20
+ The following team members contributed to this release:
21
+ @arminmeh, @brijeshb42, @dav-is, @JCQuintas, @sai6855
22
+
23
+ ### Data Grid
24
+
25
+ #### `@mui/x-data-grid@8.27.4`
26
+
27
+ - [DataGrid] Prevent unnecessary row selection checkbox rerendering (#21571) @arminmeh
28
+ - [DataGrid] Make `GridScrollArea` overrides resolver dynamic (#21612) @sai6855
29
+ - [l10n] Improve Ukrainian (uk-UA) locale (#21381) @lion1963
30
+
31
+ #### `@mui/x-data-grid-pro@8.27.4` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
32
+
33
+ Same changes as in `@mui/x-data-grid@8.27.4`, plus:
34
+
35
+ - [DataGridPro] Use `getRowId` prop to calculate the tree data row update (#21544) @arminmeh
36
+
37
+ #### `@mui/x-data-grid-premium@8.27.4` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
38
+
39
+ Same changes as in `@mui/x-data-grid-pro@8.27.4`.
40
+
41
+ ### Charts
42
+
43
+ #### `@mui/x-charts@8.27.4`
44
+
45
+ - [charts] Deprecate `ChartDataProvider` in favour of `ChartsDataProvider` (#21549) @JCQuintas
46
+ - [charts] Rename `ChartContainer` to `ChartsContainer` (#21186) @JCQuintas
47
+ - [charts] Rename `ChartZoomSlider` to `ChartsZoomSlider` (#21572) @JCQuintas
48
+
49
+ #### `@mui/x-charts-pro@8.27.4` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
50
+
51
+ Same changes as in `@mui/x-charts@8.27.4`.
52
+
53
+ #### `@mui/x-charts-premium@8.27.4` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
54
+
55
+ Same changes as in `@mui/x-charts-pro@8.27.4`.
56
+
57
+ ### Core
58
+
59
+ - [code-infra] Add eslint rule to prevent `Math.random` in docs (#21505) (#21563) @JCQuintas
60
+ - [code-infra] V8 Dedupe (#21561) @JCQuintas
61
+ - [docs-infra] Apply Cookie Banner to v8.x Branch (#21448) @dav-is
62
+ - [code-infra] Setup checkout to do full clone for non master branches (#21624) @brijeshb42
63
+
64
+ ## 8.27.3
65
+
66
+ _Feb 25, 2026_
67
+
68
+ We'd like to extend a big thank you to the 4 contributors who made this release possible. Here are some highlights ✨:
69
+
70
+ - 🐞 Bugfixes
71
+ - ⚡️ Improved dynamic data support and cache invalidation in lazy loading for Data Grid Pro
72
+
73
+ The following team members contributed to this release:
74
+ @cherniavskii, @michelengelen, @MBilalShafi, @arminmeh
75
+
76
+ ### Data Grid
77
+
78
+ #### `@mui/x-data-grid@8.27.3`
79
+
80
+ - [DataGrid] Preserve key input during row edit when using `rowModesModel` (#21457) @michelengelen
81
+
82
+ #### `@mui/x-data-grid-pro@8.27.3` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
83
+
84
+ Same changes as in `@mui/x-data-grid@8.27.3`, plus:
85
+
86
+ - [DataGridPro] Improve dynamic data support and cache invalidation in lazy loading (#21465) @MBilalShafi
87
+
88
+ #### `@mui/x-data-grid-premium@8.27.3` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
89
+
90
+ Same changes as in `@mui/x-data-grid-pro@8.27.3`.
91
+
92
+ ### Core
93
+
94
+ - [code-infra] Do not append `x` to the last version for the compare API (#21422) @arminmeh
95
+ - [docs-infra] Fix current version detection logic (#21415) @cherniavskii
96
+
8
97
  ## 8.27.2
9
98
 
10
99
  _Feb 20, 2026_
@@ -34,7 +34,7 @@ const configuration = {
34
34
  useFilterValueGetter: apiRef => apiRef.current.getRowValue
35
35
  }
36
36
  };
37
- const releaseInfo = "MTc3MTU0NTYwMDAwMA==";
37
+ const releaseInfo = "MTc3MjY2ODgwMDAwMA==";
38
38
  const watermark = /*#__PURE__*/(0, _jsxRuntime.jsx)(_xLicense.Watermark, {
39
39
  packageName: "x-data-grid-pro",
40
40
  releaseInfo: releaseInfo
@@ -192,6 +192,13 @@ DataGridProRaw.propTypes = {
192
192
  get: _propTypes.default.func.isRequired,
193
193
  set: _propTypes.default.func.isRequired
194
194
  }),
195
+ /**
196
+ * If positive, the Data Grid will periodically revalidate data source rows by re-fetching them from the server when the cache entry has expired.
197
+ * If the refetched rows are different from the current rows, the grid will update the rows.
198
+ * Set to `0` to disable polling.
199
+ * @default 0
200
+ */
201
+ dataSourceRevalidateMs: _propTypes.default.number,
195
202
  /**
196
203
  * If above 0, the row children will be expanded up to this depth.
197
204
  * If equal to -1, all the row children will be expanded.
@@ -41,6 +41,7 @@ const DATA_GRID_PRO_PROPS_DEFAULT_VALUES = exports.DATA_GRID_PRO_PROPS_DEFAULT_V
41
41
  treeData: false,
42
42
  lazyLoading: false,
43
43
  lazyLoadingRequestThrottleMs: 500,
44
+ dataSourceRevalidateMs: 0,
44
45
  listView: false,
45
46
  multipleColumnsSortingMode: 'withModifierKey',
46
47
  pinnedColumnsSectionSeparator: 'border-and-shadow',
@@ -27,7 +27,7 @@ const configuration = {
27
27
  useFilterValueGetter: apiRef => apiRef.current.getRowValue
28
28
  }
29
29
  };
30
- const releaseInfo = "MTc3MTU0NTYwMDAwMA==";
30
+ const releaseInfo = "MTc3MjY2ODgwMDAwMA==";
31
31
  const watermark = /*#__PURE__*/_jsx(Watermark, {
32
32
  packageName: "x-data-grid-pro",
33
33
  releaseInfo: releaseInfo
@@ -185,6 +185,13 @@ DataGridProRaw.propTypes = {
185
185
  get: PropTypes.func.isRequired,
186
186
  set: PropTypes.func.isRequired
187
187
  }),
188
+ /**
189
+ * If positive, the Data Grid will periodically revalidate data source rows by re-fetching them from the server when the cache entry has expired.
190
+ * If the refetched rows are different from the current rows, the grid will update the rows.
191
+ * Set to `0` to disable polling.
192
+ * @default 0
193
+ */
194
+ dataSourceRevalidateMs: PropTypes.number,
188
195
  /**
189
196
  * If above 0, the row children will be expanded up to this depth.
190
197
  * If equal to -1, all the row children will be expanded.
@@ -33,6 +33,7 @@ export const DATA_GRID_PRO_PROPS_DEFAULT_VALUES = _extends({}, DATA_GRID_PROPS_D
33
33
  treeData: false,
34
34
  lazyLoading: false,
35
35
  lazyLoadingRequestThrottleMs: 500,
36
+ dataSourceRevalidateMs: 0,
36
37
  listView: false,
37
38
  multipleColumnsSortingMode: 'withModifierKey',
38
39
  pinnedColumnsSectionSeparator: 'border-and-shadow',
@@ -13,7 +13,7 @@ export declare const useGridDataSourceBasePro: <Api extends GridPrivateApiPro>(a
13
13
  public: GridDataSourceApiPro;
14
14
  private: GridDataSourcePrivateApiPro;
15
15
  };
16
- debouncedFetchRows: ((parentId?: import("@mui/x-data-grid").GridRowId, params?: import("@mui/x-data-grid/internals").GridDataSourceFetchRowsParams<import("@mui/x-data-grid").GridGetRowsParams>) => Promise<void>) & import("@mui/utils/debounce").Cancelable;
16
+ debouncedFetchRows: ((parentId?: GridRowId, params?: import("@mui/x-data-grid/internals").GridDataSourceFetchRowsParams<import("@mui/x-data-grid").GridGetRowsParams>) => Promise<void>) & import("@mui/utils/debounce").Cancelable;
17
17
  flatTreeStrategyProcessor: {
18
18
  strategyName: DataSourceRowsUpdateStrategy;
19
19
  group: "dataSourceRowsUpdate";
@@ -4,7 +4,7 @@ import _extends from "@babel/runtime/helpers/esm/extends";
4
4
  import * as React from 'react';
5
5
  import { isDeepEqual } from '@mui/x-internals/isDeepEqual';
6
6
  import useLazyRef from '@mui/utils/useLazyRef';
7
- import { useGridSelector, GridGetRowsError, gridRowIdSelector, gridRowNodeSelector, gridRowTreeSelector, GRID_ROOT_GROUP_ID } from '@mui/x-data-grid';
7
+ import { useGridSelector, GridGetRowsError, gridRowIdSelector, gridRowNodeSelector, gridRowTreeSelector, GRID_ROOT_GROUP_ID, gridRowsLookupSelector } from '@mui/x-data-grid';
8
8
  import { gridRowGroupsToFetchSelector, useGridDataSourceBase, CacheChunkManager, gridGetRowsParamsSelector, DataSourceRowsUpdateStrategy, GridStrategyGroup, getTreeNodeDescendants } from '@mui/x-data-grid/internals';
9
9
  import { warnOnce } from '@mui/x-internals/warning';
10
10
  import { NestedDataManager, RequestStatus, getGroupKeys } from "./utils.js";
@@ -39,6 +39,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
39
39
  debouncedFetchRows,
40
40
  strategyProcessor: flatTreeStrategyProcessor,
41
41
  events,
42
+ startPolling,
42
43
  cacheChunkManager,
43
44
  cache
44
45
  } = useGridDataSourceBase(apiRef, props, _extends({
@@ -53,6 +54,35 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
53
54
  apiRef.current.setStrategyAvailability(GridStrategyGroup.DataSource, currentStrategy, props.dataSource && !props.lazyLoading ? () => true : () => false);
54
55
  }, [apiRef, props.dataSource, props.lazyLoading, props.treeData]);
55
56
  const onDataSourceErrorProp = props.onDataSourceError;
57
+ const replaceGroupRows = React.useCallback((groupId, groupPath, rows) => {
58
+ const tree = gridRowTreeSelector(apiRef);
59
+ const rowsLookup = gridRowsLookupSelector(apiRef);
60
+ const fetchedRowIds = new Set(rows.map(row => gridRowIdSelector(apiRef, row)));
61
+ const currentGroupRows = getTreeNodeDescendants(tree, groupId, false, true);
62
+ const rowsToDelete = [];
63
+ currentGroupRows.forEach(rowId => {
64
+ if (fetchedRowIds.has(rowId)) {
65
+ return;
66
+ }
67
+ const descendants = getTreeNodeDescendants(tree, rowId, false, false);
68
+ for (let i = descendants.length - 1; i >= 0; i -= 1) {
69
+ const descendantId = descendants[i];
70
+ if (fetchedRowIds.has(descendantId)) {
71
+ continue;
72
+ }
73
+ rowsToDelete.push(_extends({}, rowsLookup[descendantId], {
74
+ _action: 'delete'
75
+ }));
76
+ }
77
+ rowsToDelete.push(_extends({}, rowsLookup[rowId], {
78
+ _action: 'delete'
79
+ }));
80
+ });
81
+ if (rowsToDelete.length > 0) {
82
+ apiRef.current.updateNestedRows(rowsToDelete, groupPath);
83
+ }
84
+ apiRef.current.updateNestedRows(rows, groupPath);
85
+ }, [apiRef]);
56
86
  const fetchRowChildren = React.useCallback(async id => {
57
87
  const pipedParams = apiRef.current.unstable_applyPipeProcessors('getRowsParams', {});
58
88
  if (!props.treeData && (pipedParams.groupFields?.length ?? 0) === 0) {
@@ -78,7 +108,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
78
108
  if (cachedData !== undefined) {
79
109
  const rows = cachedData.rows;
80
110
  nestedDataManager.setRequestSettled(id);
81
- apiRef.current.updateNestedRows(rows, rowNode.path);
111
+ replaceGroupRows(id, rowNode.path, rows);
82
112
  if (cachedData.rowCount !== undefined) {
83
113
  apiRef.current.setRowCount(cachedData.rowCount);
84
114
  }
@@ -109,23 +139,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
109
139
  if (getRowsResponse.rowCount !== undefined) {
110
140
  apiRef.current.setRowCount(getRowsResponse.rowCount);
111
141
  }
112
- // Remove existing outdated rows before setting the new ones
113
- const rowsToDelete = [];
114
- getRowsResponse.rows.forEach(row => {
115
- const rowId = gridRowIdSelector(apiRef, row);
116
- const treeNode = gridRowNodeSelector(apiRef, rowId);
117
- if (treeNode) {
118
- rowsToDelete.push({
119
- id: rowId,
120
- _action: 'delete'
121
- });
122
- }
123
- });
124
- if (rowsToDelete.length > 0) {
125
- // TODO: Make this happen in a single pass by modifying the pre-processing of the rows
126
- apiRef.current.updateNestedRows(rowsToDelete, rowNode.path);
127
- }
128
- apiRef.current.updateNestedRows(getRowsResponse.rows, rowNode.path);
142
+ replaceGroupRows(id, rowNode.path, getRowsResponse.rows);
129
143
  apiRef.current.setRowChildrenExpansion(id, true);
130
144
  } catch (error) {
131
145
  const childrenFetchError = error;
@@ -143,7 +157,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
143
157
  apiRef.current.dataSource.setChildrenLoading(id, false);
144
158
  nestedDataManager.setRequestSettled(id);
145
159
  }
146
- }, [nestedDataManager, cacheChunkManager, cache, onDataSourceErrorProp, apiRef, props.treeData, props.dataSource?.getRows]);
160
+ }, [nestedDataManager, cacheChunkManager, cache, onDataSourceErrorProp, replaceGroupRows, apiRef, props.treeData, props.dataSource?.getRows]);
147
161
  const setChildrenLoading = React.useCallback((parentId, isLoading) => {
148
162
  apiRef.current.setState(state => {
149
163
  if (!state.dataSource.loading[parentId] && isLoading === false) {
@@ -185,6 +199,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
185
199
  });
186
200
  }, [apiRef]);
187
201
  const removeChildrenRows = React.useCallback(parentId => {
202
+ const rowsLookup = gridRowsLookupSelector(apiRef);
188
203
  const rowNode = gridRowNodeSelector(apiRef, parentId);
189
204
  if (!rowNode || rowNode.type !== 'group' || rowNode.children.length === 0) {
190
205
  return;
@@ -198,10 +213,9 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
198
213
  if (node.type === 'group' && node.children.length > 0) {
199
214
  node.children.forEach(traverse);
200
215
  }
201
- removedRows.push({
202
- id: nodeId,
216
+ removedRows.push(_extends({}, rowsLookup[nodeId], {
203
217
  _action: 'delete'
204
- });
218
+ }));
205
219
  };
206
220
  rowNode.children.forEach(traverse);
207
221
  if (removedRows.length > 0) {
@@ -226,6 +240,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
226
240
  apiRef.current.setRows(response.rows);
227
241
  } else {
228
242
  const tree = gridRowTreeSelector(apiRef);
243
+ const rowsLookup = gridRowsLookupSelector(apiRef);
229
244
  // Remove existing outdated rows before setting the new ones
230
245
  // Create a set of the current root rows
231
246
  const parentRowsToDelete = new Set(getTreeNodeDescendants(tree, GRID_ROOT_GROUP_ID, false, true));
@@ -239,15 +254,13 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
239
254
  const descendants = getTreeNodeDescendants(tree, parentRowId, false, false);
240
255
  for (let i = descendants.length - 1; i >= 0; i -= 1) {
241
256
  // delete deepest descendants first
242
- rowsToDelete.push({
243
- id: descendants[i],
257
+ rowsToDelete.push(_extends({}, rowsLookup[descendants[i]], {
244
258
  _action: 'delete'
245
- });
259
+ }));
246
260
  }
247
- rowsToDelete.push({
248
- id: parentRowId,
261
+ rowsToDelete.push(_extends({}, rowsLookup[parentRowId], {
249
262
  _action: 'delete'
250
- });
263
+ }));
251
264
  });
252
265
  }
253
266
  apiRef.current.updateRows(response.rows.concat(rowsToDelete));
@@ -256,7 +269,8 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
256
269
  params: params.fetchParams,
257
270
  response
258
271
  }, true);
259
- }, [apiRef]);
272
+ startPolling();
273
+ }, [apiRef, startPolling]);
260
274
  const dataSourceApi = {
261
275
  dataSource: _extends({}, api.public.dataSource, {
262
276
  setChildrenLoading,
@@ -20,7 +20,9 @@ export declare class NestedDataManager {
20
20
  private maxConcurrentRequests;
21
21
  constructor(privateApiRef: RefObject<GridPrivateApiPro>, maxConcurrentRequests?: number);
22
22
  private processQueue;
23
- queue: (ids: GridRowId[]) => Promise<void>;
23
+ queue: (ids: GridRowId[], options?: {
24
+ showChildrenLoading?: boolean;
25
+ }) => Promise<void>;
24
26
  setRequestSettled: (id: GridRowId) => void;
25
27
  clear: () => void;
26
28
  clearPendingRequest: (id: GridRowId) => void;
@@ -38,17 +38,24 @@ export class NestedDataManager {
38
38
  this.api.fetchRowChildren(id);
39
39
  }
40
40
  };
41
- queue = async ids => {
41
+ queue = async (ids, options = {}) => {
42
+ const {
43
+ showChildrenLoading = true
44
+ } = options;
42
45
  const loadingIds = {};
43
46
  ids.forEach(id => {
44
47
  this.queuedRequests.add(id);
45
- loadingIds[id] = true;
48
+ if (showChildrenLoading) {
49
+ loadingIds[id] = true;
50
+ }
46
51
  });
47
- this.api.setState(state => _extends({}, state, {
48
- dataSource: _extends({}, state.dataSource, {
49
- loading: _extends({}, state.dataSource.loading, loadingIds)
50
- })
51
- }));
52
+ if (showChildrenLoading) {
53
+ this.api.setState(state => _extends({}, state, {
54
+ dataSource: _extends({}, state.dataSource, {
55
+ loading: _extends({}, state.dataSource.loading, loadingIds)
56
+ })
57
+ }));
58
+ }
52
59
  this.processQueue();
53
60
  };
54
61
  setRequestSettled = id => {
@@ -6,4 +6,4 @@ import type { DataGridProProcessedProps } from "../../../models/dataGridProProps
6
6
  * @requires useGridPagination (state)
7
7
  * @requires useGridScroll (method
8
8
  */
9
- export declare const useGridDataSourceLazyLoader: (privateApiRef: RefObject<GridPrivateApiPro>, props: Pick<DataGridProProcessedProps, "dataSource" | "lazyLoading" | "lazyLoadingRequestThrottleMs">) => void;
9
+ export declare const useGridDataSourceLazyLoader: (privateApiRef: RefObject<GridPrivateApiPro>, props: Pick<DataGridProProcessedProps, "dataSource" | "lazyLoading" | "lazyLoadingRequestThrottleMs" | "dataSourceRevalidateMs">) => void;
@@ -3,6 +3,7 @@
3
3
  import _extends from "@babel/runtime/helpers/esm/extends";
4
4
  import * as React from 'react';
5
5
  import { throttle } from '@mui/x-internals/throttle';
6
+ import { isDeepEqual } from '@mui/x-internals/isDeepEqual';
6
7
  import useEventCallback from '@mui/utils/useEventCallback';
7
8
  import debounce from '@mui/utils/debounce';
8
9
  import { useGridEvent, gridSortModelSelector, gridFilterModelSelector, GRID_ROOT_GROUP_ID, gridPaginationModelSelector, gridFilteredSortedRowIdsSelector, gridRowIdSelector } from '@mui/x-data-grid';
@@ -35,10 +36,42 @@ export const useGridDataSourceLazyLoader = (privateApiRef, props) => {
35
36
  const loadingTrigger = React.useRef(null);
36
37
  const rowsStale = React.useRef(false);
37
38
  const draggedRowId = React.useRef(null);
39
+ const pollingIntervalRef = React.useRef(null);
38
40
  const fetchRows = React.useCallback(params => {
39
41
  privateApiRef.current.dataSource.fetchRows(GRID_ROOT_GROUP_ID, params);
40
42
  }, [privateApiRef]);
41
43
  const debouncedFetchRows = React.useMemo(() => debounce(fetchRows, 0), [fetchRows]);
44
+ const revalidate = useEventCallback(params => {
45
+ if (rowsStale.current) {
46
+ return;
47
+ }
48
+
49
+ // Check cache first — if data is still cached, skip entirely
50
+ // (no backend call, no diffing needed)
51
+ const cache = privateApiRef.current.dataSource.cache;
52
+ const cachedResponse = cache.get(params);
53
+ if (cachedResponse !== undefined) {
54
+ return;
55
+ }
56
+
57
+ // Cache is stale/expired — fetch in background (no loading indicator)
58
+ debouncedFetchRows(params);
59
+ });
60
+ const stopPolling = React.useCallback(() => {
61
+ if (pollingIntervalRef.current !== null) {
62
+ clearInterval(pollingIntervalRef.current);
63
+ pollingIntervalRef.current = null;
64
+ }
65
+ }, []);
66
+ const startPolling = useEventCallback(params => {
67
+ stopPolling();
68
+ if (props.dataSourceRevalidateMs <= 0) {
69
+ return;
70
+ }
71
+ pollingIntervalRef.current = setInterval(() => {
72
+ revalidate(params);
73
+ }, props.dataSourceRevalidateMs);
74
+ });
42
75
  const resetGrid = React.useCallback(() => {
43
76
  privateApiRef.current.setLoading(true);
44
77
  privateApiRef.current.dataSource.cache.clear();
@@ -89,38 +122,10 @@ export const useGridDataSourceLazyLoader = (privateApiRef, props) => {
89
122
  if (rootChildrenCount === 0) {
90
123
  return;
91
124
  }
92
- const paginationModel = gridPaginationModelSelector(privateApiRef);
93
- const pageToSkip = adjustRowParams({
94
- start: renderedRowsIntervalCache.current.firstRowToRender,
95
- end: renderedRowsIntervalCache.current.lastRowToRender
96
- }, {
97
- pageSize: paginationModel.pageSize,
98
- rowCount: pageRowCount
99
- });
100
125
  let hasChanged = false;
101
- const isInitialPage = renderedRowsIntervalCache.current.firstRowToRender === 0 && renderedRowsIntervalCache.current.lastRowToRender === 0;
102
- for (let i = 0; i < rootChildrenCount; i += 1) {
103
- if (isInitialPage) {
104
- break;
105
- }
106
- // replace the rows not in the viewport with skeleton rows
107
- if (pageToSkip.start <= i && i <= pageToSkip.end || tree[rootGroupChildren[i]]?.type === 'skeletonRow' ||
108
- // ignore rows that are already skeleton rows
109
- tree[rootGroupChildren[i]]?.id === draggedRowId.current // ignore row that is being dragged (https://github.com/mui/mui-x/issues/17854)
110
- ) {
111
- continue;
112
- }
113
- const rowId = tree[rootGroupChildren[i]].id; // keep the id, so that row related state is maintained
114
- const skeletonRowNode = {
115
- type: 'skeletonRow',
116
- id: rowId,
117
- parent: GRID_ROOT_GROUP_ID,
118
- depth: 0
119
- };
120
- tree[rowId] = skeletonRowNode;
121
- hasChanged = true;
122
- }
123
126
 
127
+ // SWR: Only add skeleton padding for never-fetched positions beyond current data.
128
+ // Previously fetched rows are kept in place (not skeletonized) to avoid flicker on scroll-back.
124
129
  // Should only happen with VIEWPORT loading trigger
125
130
  if (loadingTrigger.current === LoadingTrigger.VIEWPORT) {
126
131
  // fill the grid with skeleton rows
@@ -186,39 +191,121 @@ export const useGridDataSourceLazyLoader = (privateApiRef, props) => {
186
191
  const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(privateApiRef);
187
192
  const startingIndex = typeof fetchParams.start === 'string' ? Math.max(filteredSortedRowIds.indexOf(fetchParams.start), 0) : fetchParams.start;
188
193
 
189
- // Check for duplicate rows
190
- let duplicateRowCount = 0;
191
- response.rows.forEach(row => {
192
- const rowId = gridRowIdSelector(privateApiRef, row);
193
- if (tree[rowId] || dataRowIdToModelLookup[rowId]) {
194
- const index = rootGroupChildren.indexOf(rowId);
195
- if (index !== -1) {
196
- const skeletonId = getSkeletonRowId(index);
197
- rootGroupChildren[index] = skeletonId;
198
- tree[skeletonId] = {
199
- type: 'skeletonRow',
200
- id: skeletonId,
201
- parent: GRID_ROOT_GROUP_ID,
202
- depth: 0
203
- };
194
+ // Determine if this is a background revalidation (target rows are real, not skeletons)
195
+ const firstTargetRow = rootGroupChildren[startingIndex];
196
+ const isRevalidation = firstTargetRow && tree[firstTargetRow]?.type !== 'skeletonRow';
197
+ if (isRevalidation) {
198
+ // --- SWR PATH ---
199
+ // Compare response row IDs with existing row IDs at target positions
200
+ const newRowIds = response.rows.map(row => gridRowIdSelector(privateApiRef, row));
201
+ const existingRowIds = rootGroupChildren.slice(startingIndex, startingIndex + response.rows.length);
202
+ const sameRowIds = existingRowIds.length === newRowIds.length && existingRowIds.every((id, i) => id === newRowIds[i]);
203
+ if (sameRowIds) {
204
+ // SAME ROW IDs — check for data changes only
205
+ const changedRows = response.rows.filter((newRow, i) => {
206
+ const existingRow = dataRowIdToModelLookup[existingRowIds[i]];
207
+ return !isDeepEqual(newRow, existingRow);
208
+ });
209
+ if (changedRows.length === 0) {
210
+ // No changes — skip update entirely. Cache already refreshed by fetchRows.
211
+ privateApiRef.current.setLoading(false);
212
+ return;
204
213
  }
205
- delete tree[rowId];
206
- delete dataRowIdToModelLookup[rowId];
207
- duplicateRowCount += 1;
214
+
215
+ // Efficient data-only update — no tree restructuring needed
216
+ privateApiRef.current.updateRows(changedRows);
217
+ // Cache is already updated by fetchRows in useGridDataSourceBase
218
+ } else {
219
+ // DIFFERENT ROW IDs — server returned new rows for this range
220
+ // 1. Remove old rows at target positions
221
+ for (let i = startingIndex; i < startingIndex + response.rows.length && i < rootGroupChildren.length; i += 1) {
222
+ const oldRowId = rootGroupChildren[i];
223
+ if (oldRowId && tree[oldRowId]?.type !== 'skeletonRow') {
224
+ delete tree[oldRowId];
225
+ delete dataRowIdToModelLookup[oldRowId];
226
+ const skeletonId = getSkeletonRowId(i);
227
+ rootGroupChildren[i] = skeletonId;
228
+ tree[skeletonId] = {
229
+ type: 'skeletonRow',
230
+ id: skeletonId,
231
+ parent: GRID_ROOT_GROUP_ID,
232
+ depth: 0
233
+ };
234
+ }
235
+ }
236
+
237
+ // 2. Duplicate detection for incoming rows
238
+ let duplicateRowCount = 0;
239
+ response.rows.forEach(row => {
240
+ const rowId = gridRowIdSelector(privateApiRef, row);
241
+ if (tree[rowId] || dataRowIdToModelLookup[rowId]) {
242
+ const index = rootGroupChildren.indexOf(rowId);
243
+ if (index !== -1) {
244
+ const skeletonId = getSkeletonRowId(index);
245
+ rootGroupChildren[index] = skeletonId;
246
+ tree[skeletonId] = {
247
+ type: 'skeletonRow',
248
+ id: skeletonId,
249
+ parent: GRID_ROOT_GROUP_ID,
250
+ depth: 0
251
+ };
252
+ }
253
+ delete tree[rowId];
254
+ delete dataRowIdToModelLookup[rowId];
255
+ duplicateRowCount += 1;
256
+ }
257
+ });
258
+ if (duplicateRowCount > 0) {
259
+ tree[GRID_ROOT_GROUP_ID] = _extends({}, rootGroup, {
260
+ children: rootGroupChildren
261
+ });
262
+ privateApiRef.current.setState(state => _extends({}, state, {
263
+ rows: _extends({}, state.rows, {
264
+ tree,
265
+ dataRowIdToModelLookup
266
+ })
267
+ }));
268
+ }
269
+
270
+ // 3. Replace rows
271
+ privateApiRef.current.unstable_replaceRows(startingIndex, response.rows);
208
272
  }
209
- });
210
- if (duplicateRowCount > 0) {
211
- tree[GRID_ROOT_GROUP_ID] = _extends({}, rootGroup, {
212
- children: rootGroupChildren
273
+ } else {
274
+ // --- ORIGINAL PATH (skeleton real row replacement) ---
275
+ // Check for duplicate rows
276
+ let duplicateRowCount = 0;
277
+ response.rows.forEach(row => {
278
+ const rowId = gridRowIdSelector(privateApiRef, row);
279
+ if (tree[rowId] || dataRowIdToModelLookup[rowId]) {
280
+ const index = rootGroupChildren.indexOf(rowId);
281
+ if (index !== -1) {
282
+ const skeletonId = getSkeletonRowId(index);
283
+ rootGroupChildren[index] = skeletonId;
284
+ tree[skeletonId] = {
285
+ type: 'skeletonRow',
286
+ id: skeletonId,
287
+ parent: GRID_ROOT_GROUP_ID,
288
+ depth: 0
289
+ };
290
+ }
291
+ delete tree[rowId];
292
+ delete dataRowIdToModelLookup[rowId];
293
+ duplicateRowCount += 1;
294
+ }
213
295
  });
214
- privateApiRef.current.setState(state => _extends({}, state, {
215
- rows: _extends({}, state.rows, {
216
- tree,
217
- dataRowIdToModelLookup
218
- })
219
- }));
296
+ if (duplicateRowCount > 0) {
297
+ tree[GRID_ROOT_GROUP_ID] = _extends({}, rootGroup, {
298
+ children: rootGroupChildren
299
+ });
300
+ privateApiRef.current.setState(state => _extends({}, state, {
301
+ rows: _extends({}, state.rows, {
302
+ tree,
303
+ dataRowIdToModelLookup
304
+ })
305
+ }));
306
+ }
307
+ privateApiRef.current.unstable_replaceRows(startingIndex, response.rows);
220
308
  }
221
- privateApiRef.current.unstable_replaceRows(startingIndex, response.rows);
222
309
  }
223
310
  rowsStale.current = false;
224
311
  if (loadingTrigger.current === null) {
@@ -230,8 +317,11 @@ export const useGridDataSourceLazyLoader = (privateApiRef, props) => {
230
317
  params: params.fetchParams,
231
318
  response
232
319
  }, false);
320
+ if (loadingTrigger.current === LoadingTrigger.VIEWPORT) {
321
+ startPolling(params.fetchParams);
322
+ }
233
323
  privateApiRef.current.requestPipeProcessorsApplication('hydrateRows');
234
- }, [privateApiRef, updateLoadingTrigger, addSkeletonRows]);
324
+ }, [privateApiRef, updateLoadingTrigger, addSkeletonRows, startPolling]);
235
325
  const handleRowCountChange = React.useCallback(() => {
236
326
  if (rowsStale.current || loadingTrigger.current === null) {
237
327
  return;
@@ -289,26 +379,46 @@ export const useGridDataSourceLazyLoader = (privateApiRef, props) => {
289
379
  visibleRows: currentVisibleRows.rows,
290
380
  range: renderContext
291
381
  });
382
+ const paginationModel = gridPaginationModelSelector(privateApiRef);
292
383
  if (!skeletonRowsSection) {
384
+ // SWR: No skeleton rows in viewport — all visible rows have real data.
385
+ // Schedule background revalidation if cache has expired for this range.
386
+ if (loadingTrigger.current === LoadingTrigger.VIEWPORT) {
387
+ const adjustedParams = adjustRowParams(getRowsParams, {
388
+ pageSize: paginationModel.pageSize,
389
+ rowCount: privateApiRef.current.state.pagination.rowCount
390
+ });
391
+ revalidate(adjustedParams);
392
+ startPolling(adjustedParams);
393
+ }
293
394
  return;
294
395
  }
295
396
  getRowsParams.start = skeletonRowsSection.firstRowIndex;
296
397
  getRowsParams.end = skeletonRowsSection.lastRowIndex;
297
- const paginationModel = gridPaginationModelSelector(privateApiRef);
298
398
  fetchRows(adjustRowParams(getRowsParams, {
299
399
  pageSize: paginationModel.pageSize,
300
400
  rowCount: privateApiRef.current.state.pagination.rowCount
301
401
  }));
302
- }, [privateApiRef, fetchRows]);
402
+ }, [privateApiRef, fetchRows, revalidate, startPolling]);
303
403
  const throttledHandleRenderedRowsIntervalChange = React.useMemo(() => throttle(handleRenderedRowsIntervalChange, props.lazyLoadingRequestThrottleMs), [props.lazyLoadingRequestThrottleMs, handleRenderedRowsIntervalChange]);
304
404
  React.useEffect(() => {
305
405
  return () => {
306
406
  throttledHandleRenderedRowsIntervalChange.clear();
407
+ stopPolling();
307
408
  };
308
- }, [throttledHandleRenderedRowsIntervalChange]);
409
+ }, [throttledHandleRenderedRowsIntervalChange, stopPolling]);
410
+
411
+ // Stop polling when dataSourceRevalidateMs is set to 0
412
+ React.useEffect(() => {
413
+ if (props.dataSourceRevalidateMs <= 0) {
414
+ stopPolling();
415
+ }
416
+ }, [props.dataSourceRevalidateMs, stopPolling]);
417
+ React.useEffect(() => stopPolling, [stopPolling]);
309
418
  const handleGridSortModelChange = React.useCallback(newSortModel => {
310
419
  rowsStale.current = true;
311
420
  throttledHandleRenderedRowsIntervalChange.clear();
421
+ stopPolling();
312
422
  previousLastRowIndex.current = 0;
313
423
  const paginationModel = gridPaginationModelSelector(privateApiRef);
314
424
  const filterModel = gridFilterModelSelector(privateApiRef);
@@ -320,10 +430,11 @@ export const useGridDataSourceLazyLoader = (privateApiRef, props) => {
320
430
  };
321
431
  privateApiRef.current.setLoading(true);
322
432
  debouncedFetchRows(getRowsParams);
323
- }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange]);
433
+ }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange, stopPolling]);
324
434
  const handleGridFilterModelChange = React.useCallback(newFilterModel => {
325
435
  rowsStale.current = true;
326
436
  throttledHandleRenderedRowsIntervalChange.clear();
437
+ stopPolling();
327
438
  previousLastRowIndex.current = 0;
328
439
  const paginationModel = gridPaginationModelSelector(privateApiRef);
329
440
  const sortModel = gridSortModelSelector(privateApiRef);
@@ -335,7 +446,7 @@ export const useGridDataSourceLazyLoader = (privateApiRef, props) => {
335
446
  };
336
447
  privateApiRef.current.setLoading(true);
337
448
  debouncedFetchRows(getRowsParams);
338
- }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange]);
449
+ }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange, stopPolling]);
339
450
  const handleDragStart = React.useCallback(row => {
340
451
  draggedRowId.current = row.id;
341
452
  }, []);
package/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid-pro v8.27.2
2
+ * @mui/x-data-grid-pro v8.27.4
3
3
  *
4
4
  * @license SEE LICENSE IN LICENSE
5
5
  * This source code is licensed under the SEE LICENSE IN LICENSE license found in the
@@ -13,7 +13,7 @@ export declare const useGridDataSourceBasePro: <Api extends GridPrivateApiPro>(a
13
13
  public: GridDataSourceApiPro;
14
14
  private: GridDataSourcePrivateApiPro;
15
15
  };
16
- debouncedFetchRows: ((parentId?: import("@mui/x-data-grid").GridRowId, params?: import("@mui/x-data-grid/internals").GridDataSourceFetchRowsParams<import("@mui/x-data-grid").GridGetRowsParams>) => Promise<void>) & import("@mui/utils/debounce").Cancelable;
16
+ debouncedFetchRows: ((parentId?: GridRowId, params?: import("@mui/x-data-grid/internals").GridDataSourceFetchRowsParams<import("@mui/x-data-grid").GridGetRowsParams>) => Promise<void>) & import("@mui/utils/debounce").Cancelable;
17
17
  flatTreeStrategyProcessor: {
18
18
  strategyName: DataSourceRowsUpdateStrategy;
19
19
  group: "dataSourceRowsUpdate";
@@ -46,6 +46,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
46
46
  debouncedFetchRows,
47
47
  strategyProcessor: flatTreeStrategyProcessor,
48
48
  events,
49
+ startPolling,
49
50
  cacheChunkManager,
50
51
  cache
51
52
  } = (0, _internals.useGridDataSourceBase)(apiRef, props, (0, _extends2.default)({
@@ -60,6 +61,35 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
60
61
  apiRef.current.setStrategyAvailability(_internals.GridStrategyGroup.DataSource, currentStrategy, props.dataSource && !props.lazyLoading ? () => true : () => false);
61
62
  }, [apiRef, props.dataSource, props.lazyLoading, props.treeData]);
62
63
  const onDataSourceErrorProp = props.onDataSourceError;
64
+ const replaceGroupRows = React.useCallback((groupId, groupPath, rows) => {
65
+ const tree = (0, _xDataGrid.gridRowTreeSelector)(apiRef);
66
+ const rowsLookup = (0, _xDataGrid.gridRowsLookupSelector)(apiRef);
67
+ const fetchedRowIds = new Set(rows.map(row => (0, _xDataGrid.gridRowIdSelector)(apiRef, row)));
68
+ const currentGroupRows = (0, _internals.getTreeNodeDescendants)(tree, groupId, false, true);
69
+ const rowsToDelete = [];
70
+ currentGroupRows.forEach(rowId => {
71
+ if (fetchedRowIds.has(rowId)) {
72
+ return;
73
+ }
74
+ const descendants = (0, _internals.getTreeNodeDescendants)(tree, rowId, false, false);
75
+ for (let i = descendants.length - 1; i >= 0; i -= 1) {
76
+ const descendantId = descendants[i];
77
+ if (fetchedRowIds.has(descendantId)) {
78
+ continue;
79
+ }
80
+ rowsToDelete.push((0, _extends2.default)({}, rowsLookup[descendantId], {
81
+ _action: 'delete'
82
+ }));
83
+ }
84
+ rowsToDelete.push((0, _extends2.default)({}, rowsLookup[rowId], {
85
+ _action: 'delete'
86
+ }));
87
+ });
88
+ if (rowsToDelete.length > 0) {
89
+ apiRef.current.updateNestedRows(rowsToDelete, groupPath);
90
+ }
91
+ apiRef.current.updateNestedRows(rows, groupPath);
92
+ }, [apiRef]);
63
93
  const fetchRowChildren = React.useCallback(async id => {
64
94
  const pipedParams = apiRef.current.unstable_applyPipeProcessors('getRowsParams', {});
65
95
  if (!props.treeData && (pipedParams.groupFields?.length ?? 0) === 0) {
@@ -85,7 +115,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
85
115
  if (cachedData !== undefined) {
86
116
  const rows = cachedData.rows;
87
117
  nestedDataManager.setRequestSettled(id);
88
- apiRef.current.updateNestedRows(rows, rowNode.path);
118
+ replaceGroupRows(id, rowNode.path, rows);
89
119
  if (cachedData.rowCount !== undefined) {
90
120
  apiRef.current.setRowCount(cachedData.rowCount);
91
121
  }
@@ -116,23 +146,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
116
146
  if (getRowsResponse.rowCount !== undefined) {
117
147
  apiRef.current.setRowCount(getRowsResponse.rowCount);
118
148
  }
119
- // Remove existing outdated rows before setting the new ones
120
- const rowsToDelete = [];
121
- getRowsResponse.rows.forEach(row => {
122
- const rowId = (0, _xDataGrid.gridRowIdSelector)(apiRef, row);
123
- const treeNode = (0, _xDataGrid.gridRowNodeSelector)(apiRef, rowId);
124
- if (treeNode) {
125
- rowsToDelete.push({
126
- id: rowId,
127
- _action: 'delete'
128
- });
129
- }
130
- });
131
- if (rowsToDelete.length > 0) {
132
- // TODO: Make this happen in a single pass by modifying the pre-processing of the rows
133
- apiRef.current.updateNestedRows(rowsToDelete, rowNode.path);
134
- }
135
- apiRef.current.updateNestedRows(getRowsResponse.rows, rowNode.path);
149
+ replaceGroupRows(id, rowNode.path, getRowsResponse.rows);
136
150
  apiRef.current.setRowChildrenExpansion(id, true);
137
151
  } catch (error) {
138
152
  const childrenFetchError = error;
@@ -150,7 +164,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
150
164
  apiRef.current.dataSource.setChildrenLoading(id, false);
151
165
  nestedDataManager.setRequestSettled(id);
152
166
  }
153
- }, [nestedDataManager, cacheChunkManager, cache, onDataSourceErrorProp, apiRef, props.treeData, props.dataSource?.getRows]);
167
+ }, [nestedDataManager, cacheChunkManager, cache, onDataSourceErrorProp, replaceGroupRows, apiRef, props.treeData, props.dataSource?.getRows]);
154
168
  const setChildrenLoading = React.useCallback((parentId, isLoading) => {
155
169
  apiRef.current.setState(state => {
156
170
  if (!state.dataSource.loading[parentId] && isLoading === false) {
@@ -192,6 +206,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
192
206
  });
193
207
  }, [apiRef]);
194
208
  const removeChildrenRows = React.useCallback(parentId => {
209
+ const rowsLookup = (0, _xDataGrid.gridRowsLookupSelector)(apiRef);
195
210
  const rowNode = (0, _xDataGrid.gridRowNodeSelector)(apiRef, parentId);
196
211
  if (!rowNode || rowNode.type !== 'group' || rowNode.children.length === 0) {
197
212
  return;
@@ -205,10 +220,9 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
205
220
  if (node.type === 'group' && node.children.length > 0) {
206
221
  node.children.forEach(traverse);
207
222
  }
208
- removedRows.push({
209
- id: nodeId,
223
+ removedRows.push((0, _extends2.default)({}, rowsLookup[nodeId], {
210
224
  _action: 'delete'
211
- });
225
+ }));
212
226
  };
213
227
  rowNode.children.forEach(traverse);
214
228
  if (removedRows.length > 0) {
@@ -233,6 +247,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
233
247
  apiRef.current.setRows(response.rows);
234
248
  } else {
235
249
  const tree = (0, _xDataGrid.gridRowTreeSelector)(apiRef);
250
+ const rowsLookup = (0, _xDataGrid.gridRowsLookupSelector)(apiRef);
236
251
  // Remove existing outdated rows before setting the new ones
237
252
  // Create a set of the current root rows
238
253
  const parentRowsToDelete = new Set((0, _internals.getTreeNodeDescendants)(tree, _xDataGrid.GRID_ROOT_GROUP_ID, false, true));
@@ -246,15 +261,13 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
246
261
  const descendants = (0, _internals.getTreeNodeDescendants)(tree, parentRowId, false, false);
247
262
  for (let i = descendants.length - 1; i >= 0; i -= 1) {
248
263
  // delete deepest descendants first
249
- rowsToDelete.push({
250
- id: descendants[i],
264
+ rowsToDelete.push((0, _extends2.default)({}, rowsLookup[descendants[i]], {
251
265
  _action: 'delete'
252
- });
266
+ }));
253
267
  }
254
- rowsToDelete.push({
255
- id: parentRowId,
268
+ rowsToDelete.push((0, _extends2.default)({}, rowsLookup[parentRowId], {
256
269
  _action: 'delete'
257
- });
270
+ }));
258
271
  });
259
272
  }
260
273
  apiRef.current.updateRows(response.rows.concat(rowsToDelete));
@@ -263,7 +276,8 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
263
276
  params: params.fetchParams,
264
277
  response
265
278
  }, true);
266
- }, [apiRef]);
279
+ startPolling();
280
+ }, [apiRef, startPolling]);
267
281
  const dataSourceApi = {
268
282
  dataSource: (0, _extends2.default)({}, api.public.dataSource, {
269
283
  setChildrenLoading,
@@ -20,7 +20,9 @@ export declare class NestedDataManager {
20
20
  private maxConcurrentRequests;
21
21
  constructor(privateApiRef: RefObject<GridPrivateApiPro>, maxConcurrentRequests?: number);
22
22
  private processQueue;
23
- queue: (ids: GridRowId[]) => Promise<void>;
23
+ queue: (ids: GridRowId[], options?: {
24
+ showChildrenLoading?: boolean;
25
+ }) => Promise<void>;
24
26
  setRequestSettled: (id: GridRowId) => void;
25
27
  clear: () => void;
26
28
  clearPendingRequest: (id: GridRowId) => void;
@@ -44,17 +44,24 @@ class NestedDataManager {
44
44
  this.api.fetchRowChildren(id);
45
45
  }
46
46
  };
47
- queue = async ids => {
47
+ queue = async (ids, options = {}) => {
48
+ const {
49
+ showChildrenLoading = true
50
+ } = options;
48
51
  const loadingIds = {};
49
52
  ids.forEach(id => {
50
53
  this.queuedRequests.add(id);
51
- loadingIds[id] = true;
54
+ if (showChildrenLoading) {
55
+ loadingIds[id] = true;
56
+ }
52
57
  });
53
- this.api.setState(state => (0, _extends2.default)({}, state, {
54
- dataSource: (0, _extends2.default)({}, state.dataSource, {
55
- loading: (0, _extends2.default)({}, state.dataSource.loading, loadingIds)
56
- })
57
- }));
58
+ if (showChildrenLoading) {
59
+ this.api.setState(state => (0, _extends2.default)({}, state, {
60
+ dataSource: (0, _extends2.default)({}, state.dataSource, {
61
+ loading: (0, _extends2.default)({}, state.dataSource.loading, loadingIds)
62
+ })
63
+ }));
64
+ }
58
65
  this.processQueue();
59
66
  };
60
67
  setRequestSettled = id => {
@@ -6,4 +6,4 @@ import type { DataGridProProcessedProps } from "../../../models/dataGridProProps
6
6
  * @requires useGridPagination (state)
7
7
  * @requires useGridScroll (method
8
8
  */
9
- export declare const useGridDataSourceLazyLoader: (privateApiRef: RefObject<GridPrivateApiPro>, props: Pick<DataGridProProcessedProps, "dataSource" | "lazyLoading" | "lazyLoadingRequestThrottleMs">) => void;
9
+ export declare const useGridDataSourceLazyLoader: (privateApiRef: RefObject<GridPrivateApiPro>, props: Pick<DataGridProProcessedProps, "dataSource" | "lazyLoading" | "lazyLoadingRequestThrottleMs" | "dataSourceRevalidateMs">) => void;
@@ -10,6 +10,7 @@ exports.useGridDataSourceLazyLoader = void 0;
10
10
  var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
11
11
  var React = _interopRequireWildcard(require("react"));
12
12
  var _throttle = require("@mui/x-internals/throttle");
13
+ var _isDeepEqual = require("@mui/x-internals/isDeepEqual");
13
14
  var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
14
15
  var _debounce = _interopRequireDefault(require("@mui/utils/debounce"));
15
16
  var _xDataGrid = require("@mui/x-data-grid");
@@ -42,10 +43,42 @@ const useGridDataSourceLazyLoader = (privateApiRef, props) => {
42
43
  const loadingTrigger = React.useRef(null);
43
44
  const rowsStale = React.useRef(false);
44
45
  const draggedRowId = React.useRef(null);
46
+ const pollingIntervalRef = React.useRef(null);
45
47
  const fetchRows = React.useCallback(params => {
46
48
  privateApiRef.current.dataSource.fetchRows(_xDataGrid.GRID_ROOT_GROUP_ID, params);
47
49
  }, [privateApiRef]);
48
50
  const debouncedFetchRows = React.useMemo(() => (0, _debounce.default)(fetchRows, 0), [fetchRows]);
51
+ const revalidate = (0, _useEventCallback.default)(params => {
52
+ if (rowsStale.current) {
53
+ return;
54
+ }
55
+
56
+ // Check cache first — if data is still cached, skip entirely
57
+ // (no backend call, no diffing needed)
58
+ const cache = privateApiRef.current.dataSource.cache;
59
+ const cachedResponse = cache.get(params);
60
+ if (cachedResponse !== undefined) {
61
+ return;
62
+ }
63
+
64
+ // Cache is stale/expired — fetch in background (no loading indicator)
65
+ debouncedFetchRows(params);
66
+ });
67
+ const stopPolling = React.useCallback(() => {
68
+ if (pollingIntervalRef.current !== null) {
69
+ clearInterval(pollingIntervalRef.current);
70
+ pollingIntervalRef.current = null;
71
+ }
72
+ }, []);
73
+ const startPolling = (0, _useEventCallback.default)(params => {
74
+ stopPolling();
75
+ if (props.dataSourceRevalidateMs <= 0) {
76
+ return;
77
+ }
78
+ pollingIntervalRef.current = setInterval(() => {
79
+ revalidate(params);
80
+ }, props.dataSourceRevalidateMs);
81
+ });
49
82
  const resetGrid = React.useCallback(() => {
50
83
  privateApiRef.current.setLoading(true);
51
84
  privateApiRef.current.dataSource.cache.clear();
@@ -96,38 +129,10 @@ const useGridDataSourceLazyLoader = (privateApiRef, props) => {
96
129
  if (rootChildrenCount === 0) {
97
130
  return;
98
131
  }
99
- const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef);
100
- const pageToSkip = (0, _utils.adjustRowParams)({
101
- start: renderedRowsIntervalCache.current.firstRowToRender,
102
- end: renderedRowsIntervalCache.current.lastRowToRender
103
- }, {
104
- pageSize: paginationModel.pageSize,
105
- rowCount: pageRowCount
106
- });
107
132
  let hasChanged = false;
108
- const isInitialPage = renderedRowsIntervalCache.current.firstRowToRender === 0 && renderedRowsIntervalCache.current.lastRowToRender === 0;
109
- for (let i = 0; i < rootChildrenCount; i += 1) {
110
- if (isInitialPage) {
111
- break;
112
- }
113
- // replace the rows not in the viewport with skeleton rows
114
- if (pageToSkip.start <= i && i <= pageToSkip.end || tree[rootGroupChildren[i]]?.type === 'skeletonRow' ||
115
- // ignore rows that are already skeleton rows
116
- tree[rootGroupChildren[i]]?.id === draggedRowId.current // ignore row that is being dragged (https://github.com/mui/mui-x/issues/17854)
117
- ) {
118
- continue;
119
- }
120
- const rowId = tree[rootGroupChildren[i]].id; // keep the id, so that row related state is maintained
121
- const skeletonRowNode = {
122
- type: 'skeletonRow',
123
- id: rowId,
124
- parent: _xDataGrid.GRID_ROOT_GROUP_ID,
125
- depth: 0
126
- };
127
- tree[rowId] = skeletonRowNode;
128
- hasChanged = true;
129
- }
130
133
 
134
+ // SWR: Only add skeleton padding for never-fetched positions beyond current data.
135
+ // Previously fetched rows are kept in place (not skeletonized) to avoid flicker on scroll-back.
131
136
  // Should only happen with VIEWPORT loading trigger
132
137
  if (loadingTrigger.current === LoadingTrigger.VIEWPORT) {
133
138
  // fill the grid with skeleton rows
@@ -193,39 +198,121 @@ const useGridDataSourceLazyLoader = (privateApiRef, props) => {
193
198
  const filteredSortedRowIds = (0, _xDataGrid.gridFilteredSortedRowIdsSelector)(privateApiRef);
194
199
  const startingIndex = typeof fetchParams.start === 'string' ? Math.max(filteredSortedRowIds.indexOf(fetchParams.start), 0) : fetchParams.start;
195
200
 
196
- // Check for duplicate rows
197
- let duplicateRowCount = 0;
198
- response.rows.forEach(row => {
199
- const rowId = (0, _xDataGrid.gridRowIdSelector)(privateApiRef, row);
200
- if (tree[rowId] || dataRowIdToModelLookup[rowId]) {
201
- const index = rootGroupChildren.indexOf(rowId);
202
- if (index !== -1) {
203
- const skeletonId = getSkeletonRowId(index);
204
- rootGroupChildren[index] = skeletonId;
205
- tree[skeletonId] = {
206
- type: 'skeletonRow',
207
- id: skeletonId,
208
- parent: _xDataGrid.GRID_ROOT_GROUP_ID,
209
- depth: 0
210
- };
201
+ // Determine if this is a background revalidation (target rows are real, not skeletons)
202
+ const firstTargetRow = rootGroupChildren[startingIndex];
203
+ const isRevalidation = firstTargetRow && tree[firstTargetRow]?.type !== 'skeletonRow';
204
+ if (isRevalidation) {
205
+ // --- SWR PATH ---
206
+ // Compare response row IDs with existing row IDs at target positions
207
+ const newRowIds = response.rows.map(row => (0, _xDataGrid.gridRowIdSelector)(privateApiRef, row));
208
+ const existingRowIds = rootGroupChildren.slice(startingIndex, startingIndex + response.rows.length);
209
+ const sameRowIds = existingRowIds.length === newRowIds.length && existingRowIds.every((id, i) => id === newRowIds[i]);
210
+ if (sameRowIds) {
211
+ // SAME ROW IDs — check for data changes only
212
+ const changedRows = response.rows.filter((newRow, i) => {
213
+ const existingRow = dataRowIdToModelLookup[existingRowIds[i]];
214
+ return !(0, _isDeepEqual.isDeepEqual)(newRow, existingRow);
215
+ });
216
+ if (changedRows.length === 0) {
217
+ // No changes — skip update entirely. Cache already refreshed by fetchRows.
218
+ privateApiRef.current.setLoading(false);
219
+ return;
211
220
  }
212
- delete tree[rowId];
213
- delete dataRowIdToModelLookup[rowId];
214
- duplicateRowCount += 1;
221
+
222
+ // Efficient data-only update — no tree restructuring needed
223
+ privateApiRef.current.updateRows(changedRows);
224
+ // Cache is already updated by fetchRows in useGridDataSourceBase
225
+ } else {
226
+ // DIFFERENT ROW IDs — server returned new rows for this range
227
+ // 1. Remove old rows at target positions
228
+ for (let i = startingIndex; i < startingIndex + response.rows.length && i < rootGroupChildren.length; i += 1) {
229
+ const oldRowId = rootGroupChildren[i];
230
+ if (oldRowId && tree[oldRowId]?.type !== 'skeletonRow') {
231
+ delete tree[oldRowId];
232
+ delete dataRowIdToModelLookup[oldRowId];
233
+ const skeletonId = getSkeletonRowId(i);
234
+ rootGroupChildren[i] = skeletonId;
235
+ tree[skeletonId] = {
236
+ type: 'skeletonRow',
237
+ id: skeletonId,
238
+ parent: _xDataGrid.GRID_ROOT_GROUP_ID,
239
+ depth: 0
240
+ };
241
+ }
242
+ }
243
+
244
+ // 2. Duplicate detection for incoming rows
245
+ let duplicateRowCount = 0;
246
+ response.rows.forEach(row => {
247
+ const rowId = (0, _xDataGrid.gridRowIdSelector)(privateApiRef, row);
248
+ if (tree[rowId] || dataRowIdToModelLookup[rowId]) {
249
+ const index = rootGroupChildren.indexOf(rowId);
250
+ if (index !== -1) {
251
+ const skeletonId = getSkeletonRowId(index);
252
+ rootGroupChildren[index] = skeletonId;
253
+ tree[skeletonId] = {
254
+ type: 'skeletonRow',
255
+ id: skeletonId,
256
+ parent: _xDataGrid.GRID_ROOT_GROUP_ID,
257
+ depth: 0
258
+ };
259
+ }
260
+ delete tree[rowId];
261
+ delete dataRowIdToModelLookup[rowId];
262
+ duplicateRowCount += 1;
263
+ }
264
+ });
265
+ if (duplicateRowCount > 0) {
266
+ tree[_xDataGrid.GRID_ROOT_GROUP_ID] = (0, _extends2.default)({}, rootGroup, {
267
+ children: rootGroupChildren
268
+ });
269
+ privateApiRef.current.setState(state => (0, _extends2.default)({}, state, {
270
+ rows: (0, _extends2.default)({}, state.rows, {
271
+ tree,
272
+ dataRowIdToModelLookup
273
+ })
274
+ }));
275
+ }
276
+
277
+ // 3. Replace rows
278
+ privateApiRef.current.unstable_replaceRows(startingIndex, response.rows);
215
279
  }
216
- });
217
- if (duplicateRowCount > 0) {
218
- tree[_xDataGrid.GRID_ROOT_GROUP_ID] = (0, _extends2.default)({}, rootGroup, {
219
- children: rootGroupChildren
280
+ } else {
281
+ // --- ORIGINAL PATH (skeleton real row replacement) ---
282
+ // Check for duplicate rows
283
+ let duplicateRowCount = 0;
284
+ response.rows.forEach(row => {
285
+ const rowId = (0, _xDataGrid.gridRowIdSelector)(privateApiRef, row);
286
+ if (tree[rowId] || dataRowIdToModelLookup[rowId]) {
287
+ const index = rootGroupChildren.indexOf(rowId);
288
+ if (index !== -1) {
289
+ const skeletonId = getSkeletonRowId(index);
290
+ rootGroupChildren[index] = skeletonId;
291
+ tree[skeletonId] = {
292
+ type: 'skeletonRow',
293
+ id: skeletonId,
294
+ parent: _xDataGrid.GRID_ROOT_GROUP_ID,
295
+ depth: 0
296
+ };
297
+ }
298
+ delete tree[rowId];
299
+ delete dataRowIdToModelLookup[rowId];
300
+ duplicateRowCount += 1;
301
+ }
220
302
  });
221
- privateApiRef.current.setState(state => (0, _extends2.default)({}, state, {
222
- rows: (0, _extends2.default)({}, state.rows, {
223
- tree,
224
- dataRowIdToModelLookup
225
- })
226
- }));
303
+ if (duplicateRowCount > 0) {
304
+ tree[_xDataGrid.GRID_ROOT_GROUP_ID] = (0, _extends2.default)({}, rootGroup, {
305
+ children: rootGroupChildren
306
+ });
307
+ privateApiRef.current.setState(state => (0, _extends2.default)({}, state, {
308
+ rows: (0, _extends2.default)({}, state.rows, {
309
+ tree,
310
+ dataRowIdToModelLookup
311
+ })
312
+ }));
313
+ }
314
+ privateApiRef.current.unstable_replaceRows(startingIndex, response.rows);
227
315
  }
228
- privateApiRef.current.unstable_replaceRows(startingIndex, response.rows);
229
316
  }
230
317
  rowsStale.current = false;
231
318
  if (loadingTrigger.current === null) {
@@ -237,8 +324,11 @@ const useGridDataSourceLazyLoader = (privateApiRef, props) => {
237
324
  params: params.fetchParams,
238
325
  response
239
326
  }, false);
327
+ if (loadingTrigger.current === LoadingTrigger.VIEWPORT) {
328
+ startPolling(params.fetchParams);
329
+ }
240
330
  privateApiRef.current.requestPipeProcessorsApplication('hydrateRows');
241
- }, [privateApiRef, updateLoadingTrigger, addSkeletonRows]);
331
+ }, [privateApiRef, updateLoadingTrigger, addSkeletonRows, startPolling]);
242
332
  const handleRowCountChange = React.useCallback(() => {
243
333
  if (rowsStale.current || loadingTrigger.current === null) {
244
334
  return;
@@ -296,26 +386,46 @@ const useGridDataSourceLazyLoader = (privateApiRef, props) => {
296
386
  visibleRows: currentVisibleRows.rows,
297
387
  range: renderContext
298
388
  });
389
+ const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef);
299
390
  if (!skeletonRowsSection) {
391
+ // SWR: No skeleton rows in viewport — all visible rows have real data.
392
+ // Schedule background revalidation if cache has expired for this range.
393
+ if (loadingTrigger.current === LoadingTrigger.VIEWPORT) {
394
+ const adjustedParams = (0, _utils.adjustRowParams)(getRowsParams, {
395
+ pageSize: paginationModel.pageSize,
396
+ rowCount: privateApiRef.current.state.pagination.rowCount
397
+ });
398
+ revalidate(adjustedParams);
399
+ startPolling(adjustedParams);
400
+ }
300
401
  return;
301
402
  }
302
403
  getRowsParams.start = skeletonRowsSection.firstRowIndex;
303
404
  getRowsParams.end = skeletonRowsSection.lastRowIndex;
304
- const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef);
305
405
  fetchRows((0, _utils.adjustRowParams)(getRowsParams, {
306
406
  pageSize: paginationModel.pageSize,
307
407
  rowCount: privateApiRef.current.state.pagination.rowCount
308
408
  }));
309
- }, [privateApiRef, fetchRows]);
409
+ }, [privateApiRef, fetchRows, revalidate, startPolling]);
310
410
  const throttledHandleRenderedRowsIntervalChange = React.useMemo(() => (0, _throttle.throttle)(handleRenderedRowsIntervalChange, props.lazyLoadingRequestThrottleMs), [props.lazyLoadingRequestThrottleMs, handleRenderedRowsIntervalChange]);
311
411
  React.useEffect(() => {
312
412
  return () => {
313
413
  throttledHandleRenderedRowsIntervalChange.clear();
414
+ stopPolling();
314
415
  };
315
- }, [throttledHandleRenderedRowsIntervalChange]);
416
+ }, [throttledHandleRenderedRowsIntervalChange, stopPolling]);
417
+
418
+ // Stop polling when dataSourceRevalidateMs is set to 0
419
+ React.useEffect(() => {
420
+ if (props.dataSourceRevalidateMs <= 0) {
421
+ stopPolling();
422
+ }
423
+ }, [props.dataSourceRevalidateMs, stopPolling]);
424
+ React.useEffect(() => stopPolling, [stopPolling]);
316
425
  const handleGridSortModelChange = React.useCallback(newSortModel => {
317
426
  rowsStale.current = true;
318
427
  throttledHandleRenderedRowsIntervalChange.clear();
428
+ stopPolling();
319
429
  previousLastRowIndex.current = 0;
320
430
  const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef);
321
431
  const filterModel = (0, _xDataGrid.gridFilterModelSelector)(privateApiRef);
@@ -327,10 +437,11 @@ const useGridDataSourceLazyLoader = (privateApiRef, props) => {
327
437
  };
328
438
  privateApiRef.current.setLoading(true);
329
439
  debouncedFetchRows(getRowsParams);
330
- }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange]);
440
+ }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange, stopPolling]);
331
441
  const handleGridFilterModelChange = React.useCallback(newFilterModel => {
332
442
  rowsStale.current = true;
333
443
  throttledHandleRenderedRowsIntervalChange.clear();
444
+ stopPolling();
334
445
  previousLastRowIndex.current = 0;
335
446
  const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef);
336
447
  const sortModel = (0, _xDataGrid.gridSortModelSelector)(privateApiRef);
@@ -342,7 +453,7 @@ const useGridDataSourceLazyLoader = (privateApiRef, props) => {
342
453
  };
343
454
  privateApiRef.current.setLoading(true);
344
455
  debouncedFetchRows(getRowsParams);
345
- }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange]);
456
+ }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange, stopPolling]);
346
457
  const handleDragStart = React.useCallback(row => {
347
458
  draggedRowId.current = row.id;
348
459
  }, []);
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid-pro v8.27.2
2
+ * @mui/x-data-grid-pro v8.27.4
3
3
  *
4
4
  * @license SEE LICENSE IN LICENSE
5
5
  * This source code is licensed under the SEE LICENSE IN LICENSE license found in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/x-data-grid-pro",
3
- "version": "8.27.2",
3
+ "version": "8.27.4",
4
4
  "author": "MUI Team",
5
5
  "description": "The Pro plan edition of the MUI X Data Grid components.",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -37,9 +37,9 @@
37
37
  "@mui/utils": "^7.3.5",
38
38
  "clsx": "^2.1.1",
39
39
  "prop-types": "^15.8.1",
40
+ "@mui/x-internals": "8.26.0",
40
41
  "@mui/x-license": "8.26.0",
41
- "@mui/x-data-grid": "8.27.1",
42
- "@mui/x-internals": "8.26.0"
42
+ "@mui/x-data-grid": "8.27.4"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@emotion/react": "^11.9.0",