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

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,39 @@
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.3
9
+
10
+ _Feb 25, 2026_
11
+
12
+ We'd like to extend a big thank you to the 4 contributors who made this release possible. Here are some highlights ✨:
13
+
14
+ - 🐞 Bugfixes
15
+ - ⚡️ Improved dynamic data support and cache invalidation in lazy loading for Data Grid Pro
16
+
17
+ The following team members contributed to this release:
18
+ @cherniavskii, @michelengelen, @MBilalShafi, @arminmeh
19
+
20
+ ### Data Grid
21
+
22
+ #### `@mui/x-data-grid@8.27.3`
23
+
24
+ - [DataGrid] Preserve key input during row edit when using `rowModesModel` (#21457) @michelengelen
25
+
26
+ #### `@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')
27
+
28
+ Same changes as in `@mui/x-data-grid@8.27.3`, plus:
29
+
30
+ - [DataGridPro] Improve dynamic data support and cache invalidation in lazy loading (#21465) @MBilalShafi
31
+
32
+ #### `@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')
33
+
34
+ Same changes as in `@mui/x-data-grid-pro@8.27.3`.
35
+
36
+ ### Core
37
+
38
+ - [code-infra] Do not append `x` to the last version for the compare API (#21422) @arminmeh
39
+ - [docs-infra] Fix current version detection logic (#21415) @cherniavskii
40
+
8
41
  ## 8.27.2
9
42
 
10
43
  _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 = "MTc3MTk3NzYwMDAwMA==";
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 = "MTc3MTk3NzYwMDAwMA==";
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";
@@ -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,36 @@ 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 fetchedRowIds = new Set(rows.map(row => gridRowIdSelector(apiRef, row)));
60
+ const currentGroupRows = getTreeNodeDescendants(tree, groupId, false, true);
61
+ const rowsToDelete = [];
62
+ currentGroupRows.forEach(rowId => {
63
+ if (fetchedRowIds.has(rowId)) {
64
+ return;
65
+ }
66
+ const descendants = getTreeNodeDescendants(tree, rowId, false, false);
67
+ for (let i = descendants.length - 1; i >= 0; i -= 1) {
68
+ const descendantId = descendants[i];
69
+ if (fetchedRowIds.has(descendantId)) {
70
+ continue;
71
+ }
72
+ rowsToDelete.push({
73
+ id: descendantId,
74
+ _action: 'delete'
75
+ });
76
+ }
77
+ rowsToDelete.push({
78
+ id: rowId,
79
+ _action: 'delete'
80
+ });
81
+ });
82
+ if (rowsToDelete.length > 0) {
83
+ apiRef.current.updateNestedRows(rowsToDelete, groupPath);
84
+ }
85
+ apiRef.current.updateNestedRows(rows, groupPath);
86
+ }, [apiRef]);
56
87
  const fetchRowChildren = React.useCallback(async id => {
57
88
  const pipedParams = apiRef.current.unstable_applyPipeProcessors('getRowsParams', {});
58
89
  if (!props.treeData && (pipedParams.groupFields?.length ?? 0) === 0) {
@@ -78,7 +109,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
78
109
  if (cachedData !== undefined) {
79
110
  const rows = cachedData.rows;
80
111
  nestedDataManager.setRequestSettled(id);
81
- apiRef.current.updateNestedRows(rows, rowNode.path);
112
+ replaceGroupRows(id, rowNode.path, rows);
82
113
  if (cachedData.rowCount !== undefined) {
83
114
  apiRef.current.setRowCount(cachedData.rowCount);
84
115
  }
@@ -109,23 +140,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
109
140
  if (getRowsResponse.rowCount !== undefined) {
110
141
  apiRef.current.setRowCount(getRowsResponse.rowCount);
111
142
  }
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);
143
+ replaceGroupRows(id, rowNode.path, getRowsResponse.rows);
129
144
  apiRef.current.setRowChildrenExpansion(id, true);
130
145
  } catch (error) {
131
146
  const childrenFetchError = error;
@@ -143,7 +158,7 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
143
158
  apiRef.current.dataSource.setChildrenLoading(id, false);
144
159
  nestedDataManager.setRequestSettled(id);
145
160
  }
146
- }, [nestedDataManager, cacheChunkManager, cache, onDataSourceErrorProp, apiRef, props.treeData, props.dataSource?.getRows]);
161
+ }, [nestedDataManager, cacheChunkManager, cache, onDataSourceErrorProp, replaceGroupRows, apiRef, props.treeData, props.dataSource?.getRows]);
147
162
  const setChildrenLoading = React.useCallback((parentId, isLoading) => {
148
163
  apiRef.current.setState(state => {
149
164
  if (!state.dataSource.loading[parentId] && isLoading === false) {
@@ -256,7 +271,8 @@ export const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
256
271
  params: params.fetchParams,
257
272
  response
258
273
  }, true);
259
- }, [apiRef]);
274
+ startPolling();
275
+ }, [apiRef, startPolling]);
260
276
  const dataSourceApi = {
261
277
  dataSource: _extends({}, api.public.dataSource, {
262
278
  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.3
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,36 @@ 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 fetchedRowIds = new Set(rows.map(row => (0, _xDataGrid.gridRowIdSelector)(apiRef, row)));
67
+ const currentGroupRows = (0, _internals.getTreeNodeDescendants)(tree, groupId, false, true);
68
+ const rowsToDelete = [];
69
+ currentGroupRows.forEach(rowId => {
70
+ if (fetchedRowIds.has(rowId)) {
71
+ return;
72
+ }
73
+ const descendants = (0, _internals.getTreeNodeDescendants)(tree, rowId, false, false);
74
+ for (let i = descendants.length - 1; i >= 0; i -= 1) {
75
+ const descendantId = descendants[i];
76
+ if (fetchedRowIds.has(descendantId)) {
77
+ continue;
78
+ }
79
+ rowsToDelete.push({
80
+ id: descendantId,
81
+ _action: 'delete'
82
+ });
83
+ }
84
+ rowsToDelete.push({
85
+ id: rowId,
86
+ _action: 'delete'
87
+ });
88
+ });
89
+ if (rowsToDelete.length > 0) {
90
+ apiRef.current.updateNestedRows(rowsToDelete, groupPath);
91
+ }
92
+ apiRef.current.updateNestedRows(rows, groupPath);
93
+ }, [apiRef]);
63
94
  const fetchRowChildren = React.useCallback(async id => {
64
95
  const pipedParams = apiRef.current.unstable_applyPipeProcessors('getRowsParams', {});
65
96
  if (!props.treeData && (pipedParams.groupFields?.length ?? 0) === 0) {
@@ -85,7 +116,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
85
116
  if (cachedData !== undefined) {
86
117
  const rows = cachedData.rows;
87
118
  nestedDataManager.setRequestSettled(id);
88
- apiRef.current.updateNestedRows(rows, rowNode.path);
119
+ replaceGroupRows(id, rowNode.path, rows);
89
120
  if (cachedData.rowCount !== undefined) {
90
121
  apiRef.current.setRowCount(cachedData.rowCount);
91
122
  }
@@ -116,23 +147,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
116
147
  if (getRowsResponse.rowCount !== undefined) {
117
148
  apiRef.current.setRowCount(getRowsResponse.rowCount);
118
149
  }
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);
150
+ replaceGroupRows(id, rowNode.path, getRowsResponse.rows);
136
151
  apiRef.current.setRowChildrenExpansion(id, true);
137
152
  } catch (error) {
138
153
  const childrenFetchError = error;
@@ -150,7 +165,7 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
150
165
  apiRef.current.dataSource.setChildrenLoading(id, false);
151
166
  nestedDataManager.setRequestSettled(id);
152
167
  }
153
- }, [nestedDataManager, cacheChunkManager, cache, onDataSourceErrorProp, apiRef, props.treeData, props.dataSource?.getRows]);
168
+ }, [nestedDataManager, cacheChunkManager, cache, onDataSourceErrorProp, replaceGroupRows, apiRef, props.treeData, props.dataSource?.getRows]);
154
169
  const setChildrenLoading = React.useCallback((parentId, isLoading) => {
155
170
  apiRef.current.setState(state => {
156
171
  if (!state.dataSource.loading[parentId] && isLoading === false) {
@@ -263,7 +278,8 @@ const useGridDataSourceBasePro = (apiRef, props, options = {}) => {
263
278
  params: params.fetchParams,
264
279
  response
265
280
  }, true);
266
- }, [apiRef]);
281
+ startPolling();
282
+ }, [apiRef, startPolling]);
267
283
  const dataSourceApi = {
268
284
  dataSource: (0, _extends2.default)({}, api.public.dataSource, {
269
285
  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.3
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.3",
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-license": "8.26.0",
41
- "@mui/x-data-grid": "8.27.1",
42
- "@mui/x-internals": "8.26.0"
40
+ "@mui/x-data-grid": "8.27.3",
41
+ "@mui/x-internals": "8.26.0",
42
+ "@mui/x-license": "8.26.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@emotion/react": "^11.9.0",