Skip to content

Commit e5d71cf

Browse files
committed
Use intersection observer to trigger server-side infinite loading
1 parent 07f73ed commit e5d71cf

File tree

8 files changed

+230
-158
lines changed

8 files changed

+230
-158
lines changed

packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
useGridLazyLoader,
6464
useGridLazyLoaderPreProcessors,
6565
useGridDataSourceLazyLoader,
66+
useGridIntersectionObserver,
6667
headerFilteringStateInitializer,
6768
useGridHeaderFiltering,
6869
virtualizationStateInitializer,
@@ -210,6 +211,7 @@ export const useDataGridPremiumComponent = (
210211
useGridInfiniteLoader(apiRef, props);
211212
useGridLazyLoader(apiRef, props);
212213
useGridDataSourceLazyLoader(apiRef, props);
214+
useGridIntersectionObserver(apiRef, props);
213215
useGridColumnMenu(apiRef);
214216
useGridCsvExport(apiRef, props);
215217
useGridPrintExport(apiRef, props);

packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import {
8888
dataSourceStateInitializer,
8989
} from '../hooks/features/dataSource/useGridDataSourcePro';
9090
import { useGridDataSourceLazyLoader } from '../hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader';
91+
import { useGridIntersectionObserver } from '../hooks/features/serverSideLazyLoader/useGridIntersectionObserver';
9192

9293
export const useDataGridProComponent = (
9394
apiRef: RefObject<GridPrivateApiPro>,
@@ -167,6 +168,7 @@ export const useDataGridProComponent = (
167168
useGridInfiniteLoader(apiRef, props);
168169
useGridLazyLoader(apiRef, props);
169170
useGridDataSourceLazyLoader(apiRef, props);
171+
useGridIntersectionObserver(apiRef, props);
170172
useGridColumnMenu(apiRef);
171173
useGridCsvExport(apiRef, props);
172174
useGridPrintExport(apiRef, props);
Lines changed: 12 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,16 @@
1-
import * as React from 'react';
21
import { RefObject } from '@mui/x-internals/types';
32
import {
43
useGridSelector,
54
useGridEventPriority,
65
gridVisibleColumnDefinitionsSelector,
7-
useGridApiMethod,
8-
gridDimensionsSelector,
6+
useGridEvent,
97
} from '@mui/x-data-grid';
10-
import {
11-
useGridVisibleRows,
12-
GridInfiniteLoaderPrivateApi,
13-
useTimeout,
14-
gridHorizontalScrollbarHeightSelector,
15-
} from '@mui/x-data-grid/internals';
8+
import { useGridVisibleRows, runIf } from '@mui/x-data-grid/internals';
169
import useEventCallback from '@mui/utils/useEventCallback';
17-
import { styled } from '@mui/system';
1810
import { GridRowScrollEndParams } from '../../../models';
1911
import { GridPrivateApiPro } from '../../../models/gridApiPro';
2012
import { DataGridProProcessedProps } from '../../../models/dataGridProProps';
2113

22-
const InfiniteLoadingTriggerElement = styled('div')({
23-
position: 'sticky',
24-
left: 0,
25-
width: 0,
26-
height: 0,
27-
});
28-
2914
/**
3015
* @requires useGridColumns (state)
3116
* @requires useGridDimensions (method) - can be after
@@ -35,110 +20,24 @@ export const useGridInfiniteLoader = (
3520
apiRef: RefObject<GridPrivateApiPro>,
3621
props: Pick<
3722
DataGridProProcessedProps,
38-
'onRowsScrollEnd' | 'pagination' | 'paginationMode' | 'rowsLoadingMode' | 'scrollEndThreshold'
23+
'onRowsScrollEnd' | 'pagination' | 'paginationMode' | 'rowsLoadingMode'
3924
>,
4025
): void => {
41-
const isReady = useGridSelector(apiRef, gridDimensionsSelector).isReady;
4226
const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector);
4327
const currentPage = useGridVisibleRows(apiRef, props);
44-
const observer = React.useRef<IntersectionObserver>(null);
45-
const updateTargetTimeout = useTimeout();
46-
const triggerElement = React.useRef<HTMLElement | null>(null);
4728

4829
const isEnabled = props.rowsLoadingMode === 'client' && !!props.onRowsScrollEnd;
4930

50-
const handleLoadMoreRows = useEventCallback(([entry]: IntersectionObserverEntry[]) => {
51-
const currentRatio = entry.intersectionRatio;
52-
const isIntersecting = entry.isIntersecting;
53-
54-
if (isIntersecting && currentRatio === 1) {
55-
const viewportPageSize = apiRef.current.getViewportPageSize();
56-
const rowScrollEndParams: GridRowScrollEndParams = {
57-
visibleColumns,
58-
viewportPageSize,
59-
visibleRowsCount: currentPage.rows.length,
60-
};
61-
apiRef.current.publishEvent('rowsScrollEnd', rowScrollEndParams);
62-
observer.current?.disconnect();
63-
// do not observe this node anymore
64-
triggerElement.current = null;
65-
}
31+
const handleLoadMoreRows = useEventCallback(() => {
32+
const viewportPageSize = apiRef.current.getViewportPageSize();
33+
const rowScrollEndParams: GridRowScrollEndParams = {
34+
visibleColumns,
35+
viewportPageSize,
36+
visibleRowsCount: currentPage.rows.length,
37+
};
38+
apiRef.current.publishEvent('rowsScrollEnd', rowScrollEndParams);
6639
});
6740

68-
React.useEffect(() => {
69-
const virtualScroller = apiRef.current.virtualScrollerRef.current;
70-
if (!isEnabled || !isReady || !virtualScroller) {
71-
return;
72-
}
73-
observer.current?.disconnect();
74-
75-
const horizontalScrollbarHeight = gridHorizontalScrollbarHeightSelector(apiRef);
76-
const marginBottom = props.scrollEndThreshold - horizontalScrollbarHeight;
77-
78-
observer.current = new IntersectionObserver(handleLoadMoreRows, {
79-
threshold: 1,
80-
root: virtualScroller,
81-
rootMargin: `0px 0px ${marginBottom}px 0px`,
82-
});
83-
if (triggerElement.current) {
84-
observer.current.observe(triggerElement.current);
85-
}
86-
}, [apiRef, isReady, handleLoadMoreRows, isEnabled, props.scrollEndThreshold]);
87-
88-
const updateTarget = (node: HTMLElement | null) => {
89-
if (triggerElement.current !== node) {
90-
observer.current?.disconnect();
91-
92-
triggerElement.current = node;
93-
if (triggerElement.current) {
94-
observer.current?.observe(triggerElement.current);
95-
}
96-
}
97-
};
98-
99-
const triggerRef = React.useCallback(
100-
(node: HTMLElement | null) => {
101-
// Prevent the infite loading working in combination with lazy loading
102-
if (!isEnabled) {
103-
return;
104-
}
105-
106-
// If the user scrolls through the grid too fast it might happen that the observer is connected to the trigger element
107-
// that will be intersecting the root inside the same render cycle (but not intersecting at the time of the connection).
108-
// This will cause the observer to not call the callback with `isIntersecting` set to `true`.
109-
// https://www.w3.org/TR/intersection-observer/#event-loop
110-
// Delaying the connection to the next cycle helps since the observer will always call the callback the first time it is connected.
111-
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/observe
112-
// Related to
113-
// https://github.com/mui/mui-x/issues/14116
114-
updateTargetTimeout.start(0, () => updateTarget(node));
115-
},
116-
[isEnabled, updateTargetTimeout],
117-
);
118-
119-
const getInfiniteLoadingTriggerElement = React.useCallback<
120-
NonNullable<GridInfiniteLoaderPrivateApi['getInfiniteLoadingTriggerElement']>
121-
>(
122-
({ lastRowId }) => {
123-
if (!isEnabled) {
124-
return null;
125-
}
126-
return (
127-
<InfiniteLoadingTriggerElement
128-
ref={triggerRef}
129-
// Force rerender on last row change to start observing the new trigger
130-
key={`trigger-${lastRowId}`}
131-
role="presentation"
132-
/>
133-
);
134-
},
135-
[isEnabled, triggerRef],
136-
);
137-
138-
const infiniteLoaderPrivateApi: GridInfiniteLoaderPrivateApi = {
139-
getInfiniteLoadingTriggerElement,
140-
};
141-
142-
useGridApiMethod(apiRef, infiniteLoaderPrivateApi, 'private');
14341
useGridEventPriority(apiRef, 'rowsScrollEnd', props.onRowsScrollEnd);
42+
useGridEvent(apiRef, 'onIntersection', runIf(isEnabled, handleLoadMoreRows));
14443
};

packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
GridGroupNode,
1212
GridSkeletonRowNode,
1313
gridPaginationModelSelector,
14-
gridDimensionsSelector,
1514
gridFilteredSortedRowIdsSelector,
1615
gridRowIdSelector,
1716
} from '@mui/x-data-grid';
@@ -45,19 +44,13 @@ const getSkeletonRowId = (index: number) => `${GRID_SKELETON_ROW_ROOT_ID}-${inde
4544
/**
4645
* @requires useGridRows (state)
4746
* @requires useGridPagination (state)
48-
* @requires useGridDimensions (method) - can be after
4947
* @requires useGridScroll (method
5048
*/
5149
export const useGridDataSourceLazyLoader = (
5250
privateApiRef: RefObject<GridPrivateApiPro>,
5351
props: Pick<
5452
DataGridProProcessedProps,
55-
| 'pagination'
56-
| 'paginationMode'
57-
| 'dataSource'
58-
| 'lazyLoading'
59-
| 'lazyLoadingRequestThrottleMs'
60-
| 'scrollEndThreshold'
53+
'dataSource' | 'lazyLoading' | 'lazyLoadingRequestThrottleMs'
6154
>,
6255
): void => {
6356
const setStrategyAvailability = React.useCallback(() => {
@@ -354,41 +347,32 @@ export const useGridDataSourceLazyLoader = (
354347
privateApiRef.current.requestPipeProcessorsApplication('hydrateRows');
355348
}, [privateApiRef, updateLoadingTrigger, addSkeletonRows]);
356349

357-
const handleScrolling: GridEventListener<'scrollPositionChange'> = React.useCallback(
358-
(newScrollPosition) => {
359-
if (rowsStale.current || loadingTrigger.current !== LoadingTrigger.SCROLL_END) {
360-
return;
361-
}
350+
const handleIntersection: GridEventListener<'onIntersection'> = React.useCallback(() => {
351+
if (rowsStale.current || loadingTrigger.current !== LoadingTrigger.SCROLL_END) {
352+
return;
353+
}
362354

363-
const renderContext = gridRenderContextSelector(privateApiRef);
364-
if (previousLastRowIndex.current >= renderContext.lastRowIndex) {
365-
return;
366-
}
355+
const renderContext = gridRenderContextSelector(privateApiRef);
356+
if (previousLastRowIndex.current >= renderContext.lastRowIndex) {
357+
return;
358+
}
367359

368-
const dimensions = gridDimensionsSelector(privateApiRef);
369-
const position = newScrollPosition.top + dimensions.viewportInnerSize.height;
370-
const target = dimensions.contentSize.height - props.scrollEndThreshold;
371-
372-
if (position >= target) {
373-
previousLastRowIndex.current = renderContext.lastRowIndex;
374-
375-
const paginationModel = gridPaginationModelSelector(privateApiRef);
376-
const sortModel = gridSortModelSelector(privateApiRef);
377-
const filterModel = gridFilterModelSelector(privateApiRef);
378-
const getRowsParams: GridGetRowsParams = {
379-
start: renderContext.lastRowIndex,
380-
end: renderContext.lastRowIndex + paginationModel.pageSize - 1,
381-
sortModel,
382-
filterModel,
383-
};
360+
previousLastRowIndex.current = renderContext.lastRowIndex;
361+
362+
const paginationModel = gridPaginationModelSelector(privateApiRef);
363+
const sortModel = gridSortModelSelector(privateApiRef);
364+
const filterModel = gridFilterModelSelector(privateApiRef);
365+
const getRowsParams: GridGetRowsParams = {
366+
start: renderContext.lastRowIndex,
367+
end: renderContext.lastRowIndex + paginationModel.pageSize - 1,
368+
sortModel,
369+
filterModel,
370+
};
384371

385-
privateApiRef.current.setLoading(true);
372+
privateApiRef.current.setLoading(true);
386373

387-
fetchRows(adjustRowParams(getRowsParams));
388-
}
389-
},
390-
[privateApiRef, props.scrollEndThreshold, adjustRowParams, fetchRows],
391-
);
374+
fetchRows(adjustRowParams(getRowsParams));
375+
}, [privateApiRef, adjustRowParams, fetchRows]);
392376

393377
const handleRenderedRowsIntervalChange = React.useCallback<
394378
GridEventListener<'renderedRowsIntervalChange'>
@@ -446,6 +430,7 @@ export const useGridDataSourceLazyLoader = (
446430
() => throttle(handleRenderedRowsIntervalChange, props.lazyLoadingRequestThrottleMs),
447431
[props.lazyLoadingRequestThrottleMs, handleRenderedRowsIntervalChange],
448432
);
433+
449434
React.useEffect(() => {
450435
return () => {
451436
throttledHandleRenderedRowsIntervalChange.clear();
@@ -519,8 +504,8 @@ export const useGridDataSourceLazyLoader = (
519504
);
520505
useGridEvent(
521506
privateApiRef,
522-
'scrollPositionChange',
523-
runIf(lazyLoadingRowsUpdateStrategyActive, handleScrolling),
507+
'onIntersection',
508+
runIf(lazyLoadingRowsUpdateStrategyActive, handleIntersection),
524509
);
525510
useGridEvent(
526511
privateApiRef,

0 commit comments

Comments
 (0)