@mui/x-data-grid-pro
Version:
The Pro plan edition of the MUI X Data Grid components.
365 lines (357 loc) • 16.4 kB
JavaScript
'use client';
import _extends from "@babel/runtime/helpers/esm/extends";
import * as React from 'react';
import { throttle } from '@mui/x-internals/throttle';
import useEventCallback from '@mui/utils/useEventCallback';
import debounce from '@mui/utils/debounce';
import { useGridEvent, gridSortModelSelector, gridFilterModelSelector, GRID_ROOT_GROUP_ID, gridPaginationModelSelector, gridFilteredSortedRowIdsSelector, gridRowIdSelector } from '@mui/x-data-grid';
import { getVisibleRows, gridRenderContextSelector, GridStrategyGroup, useGridRegisterStrategyProcessor, runIf, DataSourceRowsUpdateStrategy } from '@mui/x-data-grid/internals';
import { findSkeletonRowsSection } from "../lazyLoader/utils.js";
import { GRID_SKELETON_ROW_ROOT_ID } from "../lazyLoader/useGridLazyLoaderPreProcessors.js";
var LoadingTrigger = /*#__PURE__*/function (LoadingTrigger) {
LoadingTrigger[LoadingTrigger["VIEWPORT"] = 0] = "VIEWPORT";
LoadingTrigger[LoadingTrigger["SCROLL_END"] = 1] = "SCROLL_END";
return LoadingTrigger;
}(LoadingTrigger || {});
const INTERVAL_CACHE_INITIAL_STATE = {
firstRowToRender: 0,
lastRowToRender: 0
};
const getSkeletonRowId = index => `${GRID_SKELETON_ROW_ROOT_ID}-${index}`;
/**
* @requires useGridRows (state)
* @requires useGridPagination (state)
* @requires useGridScroll (method
*/
export const useGridDataSourceLazyLoader = (privateApiRef, props) => {
const setStrategyAvailability = React.useCallback(() => {
privateApiRef.current.setStrategyAvailability(GridStrategyGroup.DataSource, DataSourceRowsUpdateStrategy.LazyLoading, props.dataSource && props.lazyLoading ? () => true : () => false);
}, [privateApiRef, props.lazyLoading, props.dataSource]);
const [lazyLoadingRowsUpdateStrategyActive, setLazyLoadingRowsUpdateStrategyActive] = React.useState(false);
const renderedRowsIntervalCache = React.useRef(INTERVAL_CACHE_INITIAL_STATE);
const previousLastRowIndex = React.useRef(0);
const loadingTrigger = React.useRef(null);
const rowsStale = React.useRef(false);
const draggedRowId = React.useRef(null);
const fetchRows = React.useCallback(params => {
privateApiRef.current.dataSource.fetchRows(GRID_ROOT_GROUP_ID, params);
}, [privateApiRef]);
const debouncedFetchRows = React.useMemo(() => debounce(fetchRows, 0), [fetchRows]);
// Adjust the render context range to fit the pagination model's page size
// First row index should be decreased to the start of the page, end row index should be increased to the end of the page
const adjustRowParams = React.useCallback(params => {
if (typeof params.start !== 'number') {
return params;
}
const paginationModel = gridPaginationModelSelector(privateApiRef);
return _extends({}, params, {
start: params.start - params.start % paginationModel.pageSize,
end: params.end + paginationModel.pageSize - params.end % paginationModel.pageSize - 1
});
}, [privateApiRef]);
const resetGrid = React.useCallback(() => {
privateApiRef.current.setLoading(true);
privateApiRef.current.dataSource.cache.clear();
rowsStale.current = true;
previousLastRowIndex.current = 0;
const paginationModel = gridPaginationModelSelector(privateApiRef);
const sortModel = gridSortModelSelector(privateApiRef);
const filterModel = gridFilterModelSelector(privateApiRef);
const getRowsParams = {
start: 0,
end: paginationModel.pageSize - 1,
sortModel,
filterModel
};
fetchRows(getRowsParams);
}, [privateApiRef, fetchRows]);
const ensureValidRowCount = React.useCallback((previousLoadingTrigger, newLoadingTrigger) => {
// switching from lazy loading to infinite loading should always reset the grid
// since there is no guarantee that the new data will be placed correctly
// there might be some skeleton rows in between the data or the data has changed (row count became unknown)
if (previousLoadingTrigger === LoadingTrigger.VIEWPORT && newLoadingTrigger === LoadingTrigger.SCROLL_END) {
resetGrid();
return;
}
// switching from infinite loading to lazy loading should reset the grid only if the known row count
// is smaller than the amount of rows rendered
const tree = privateApiRef.current.state.rows.tree;
const rootGroup = tree[GRID_ROOT_GROUP_ID];
const rootGroupChildren = [...rootGroup.children];
const pageRowCount = privateApiRef.current.state.pagination.rowCount;
const rootChildrenCount = rootGroupChildren.length;
if (rootChildrenCount > pageRowCount) {
resetGrid();
}
}, [privateApiRef, resetGrid]);
const addSkeletonRows = React.useCallback(() => {
const tree = privateApiRef.current.state.rows.tree;
const rootGroup = tree[GRID_ROOT_GROUP_ID];
const rootGroupChildren = [...rootGroup.children];
const pageRowCount = privateApiRef.current.state.pagination.rowCount;
const rootChildrenCount = rootGroupChildren.length;
/**
* Do nothing if
* - children count is 0
*/
if (rootChildrenCount === 0) {
return;
}
const pageToSkip = adjustRowParams({
start: renderedRowsIntervalCache.current.firstRowToRender,
end: renderedRowsIntervalCache.current.lastRowToRender
});
let hasChanged = false;
const isInitialPage = renderedRowsIntervalCache.current.firstRowToRender === 0 && renderedRowsIntervalCache.current.lastRowToRender === 0;
for (let i = 0; i < rootChildrenCount; i += 1) {
if (isInitialPage) {
break;
}
// replace the rows not in the viewport with skeleton rows
if (pageToSkip.start <= i && i <= pageToSkip.end || tree[rootGroupChildren[i]]?.type === 'skeletonRow' ||
// ignore rows that are already skeleton rows
tree[rootGroupChildren[i]]?.id === draggedRowId.current // ignore row that is being dragged (https://github.com/mui/mui-x/issues/17854)
) {
continue;
}
const rowId = tree[rootGroupChildren[i]].id; // keep the id, so that row related state is maintained
const skeletonRowNode = {
type: 'skeletonRow',
id: rowId,
parent: GRID_ROOT_GROUP_ID,
depth: 0
};
tree[rowId] = skeletonRowNode;
hasChanged = true;
}
// Should only happen with VIEWPORT loading trigger
if (loadingTrigger.current === LoadingTrigger.VIEWPORT) {
// fill the grid with skeleton rows
for (let i = 0; i < pageRowCount - rootChildrenCount; i += 1) {
const skeletonId = getSkeletonRowId(i + rootChildrenCount); // to avoid duplicate keys on rebuild
rootGroupChildren.push(skeletonId);
const skeletonRowNode = {
type: 'skeletonRow',
id: skeletonId,
parent: GRID_ROOT_GROUP_ID,
depth: 0
};
tree[skeletonId] = skeletonRowNode;
hasChanged = true;
}
}
if (!hasChanged) {
return;
}
tree[GRID_ROOT_GROUP_ID] = _extends({}, rootGroup, {
children: rootGroupChildren
});
privateApiRef.current.setState(state => _extends({}, state, {
rows: _extends({}, state.rows, {
tree
})
}), 'addSkeletonRows');
}, [privateApiRef, adjustRowParams]);
const updateLoadingTrigger = React.useCallback(rowCount => {
const newLoadingTrigger = rowCount === -1 ? LoadingTrigger.SCROLL_END : LoadingTrigger.VIEWPORT;
if (loadingTrigger.current !== null) {
ensureValidRowCount(loadingTrigger.current, newLoadingTrigger);
}
if (loadingTrigger.current !== newLoadingTrigger) {
loadingTrigger.current = newLoadingTrigger;
}
}, [ensureValidRowCount]);
const handleDataUpdate = React.useCallback(params => {
if ('error' in params) {
return;
}
const {
response,
fetchParams
} = params;
const pageRowCount = privateApiRef.current.state.pagination.rowCount;
const tree = privateApiRef.current.state.rows.tree;
const dataRowIdToModelLookup = privateApiRef.current.state.rows.dataRowIdToModelLookup;
if (response.rowCount !== undefined || pageRowCount === undefined) {
privateApiRef.current.setRowCount(response.rowCount === undefined ? -1 : response.rowCount);
}
// scroll to the top if the rows are stale and the new request is for the first page
if (rowsStale.current && params.fetchParams.start === 0) {
privateApiRef.current.scroll({
top: 0
});
// the rows can safely be replaced. skeleton rows will be added later
privateApiRef.current.setRows(response.rows);
} else {
const rootGroup = tree[GRID_ROOT_GROUP_ID];
const rootGroupChildren = [...rootGroup.children];
const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(privateApiRef);
const startingIndex = typeof fetchParams.start === 'string' ? Math.max(filteredSortedRowIds.indexOf(fetchParams.start), 0) : fetchParams.start;
// Check for duplicate rows
let duplicateRowCount = 0;
response.rows.forEach(row => {
const rowId = gridRowIdSelector(privateApiRef, row);
if (tree[rowId] || dataRowIdToModelLookup[rowId]) {
const index = rootGroupChildren.indexOf(rowId);
if (index !== -1) {
const skeletonId = getSkeletonRowId(index);
rootGroupChildren[index] = skeletonId;
tree[skeletonId] = {
type: 'skeletonRow',
id: skeletonId,
parent: GRID_ROOT_GROUP_ID,
depth: 0
};
}
delete tree[rowId];
delete dataRowIdToModelLookup[rowId];
duplicateRowCount += 1;
}
});
if (duplicateRowCount > 0) {
tree[GRID_ROOT_GROUP_ID] = _extends({}, rootGroup, {
children: rootGroupChildren
});
privateApiRef.current.setState(state => _extends({}, state, {
rows: _extends({}, state.rows, {
tree,
dataRowIdToModelLookup
})
}));
}
privateApiRef.current.unstable_replaceRows(startingIndex, response.rows);
}
rowsStale.current = false;
if (loadingTrigger.current === null) {
updateLoadingTrigger(privateApiRef.current.state.pagination.rowCount);
}
addSkeletonRows();
privateApiRef.current.setLoading(false);
privateApiRef.current.unstable_applyPipeProcessors('processDataSourceRows', {
params: params.fetchParams,
response
}, false);
privateApiRef.current.requestPipeProcessorsApplication('hydrateRows');
}, [privateApiRef, updateLoadingTrigger, addSkeletonRows]);
const handleRowCountChange = React.useCallback(() => {
if (rowsStale.current || loadingTrigger.current === null) {
return;
}
updateLoadingTrigger(privateApiRef.current.state.pagination.rowCount);
addSkeletonRows();
privateApiRef.current.requestPipeProcessorsApplication('hydrateRows');
}, [privateApiRef, updateLoadingTrigger, addSkeletonRows]);
const handleIntersection = useEventCallback(() => {
if (rowsStale.current || loadingTrigger.current !== LoadingTrigger.SCROLL_END) {
return;
}
const renderContext = gridRenderContextSelector(privateApiRef);
if (previousLastRowIndex.current >= renderContext.lastRowIndex) {
return;
}
previousLastRowIndex.current = renderContext.lastRowIndex;
const paginationModel = gridPaginationModelSelector(privateApiRef);
const sortModel = gridSortModelSelector(privateApiRef);
const filterModel = gridFilterModelSelector(privateApiRef);
const getRowsParams = {
start: renderContext.lastRowIndex,
end: renderContext.lastRowIndex + paginationModel.pageSize - 1,
sortModel,
filterModel
};
privateApiRef.current.setLoading(true);
fetchRows(adjustRowParams(getRowsParams));
});
const handleRenderedRowsIntervalChange = React.useCallback(params => {
if (rowsStale.current) {
return;
}
const sortModel = gridSortModelSelector(privateApiRef);
const filterModel = gridFilterModelSelector(privateApiRef);
const getRowsParams = {
start: params.firstRowIndex,
end: params.lastRowIndex - 1,
sortModel,
filterModel
};
if (renderedRowsIntervalCache.current.firstRowToRender === params.firstRowIndex && renderedRowsIntervalCache.current.lastRowToRender === params.lastRowIndex) {
return;
}
renderedRowsIntervalCache.current = {
firstRowToRender: params.firstRowIndex,
lastRowToRender: params.lastRowIndex
};
const currentVisibleRows = getVisibleRows(privateApiRef);
const skeletonRowsSection = findSkeletonRowsSection({
apiRef: privateApiRef,
visibleRows: currentVisibleRows.rows,
range: {
firstRowIndex: params.firstRowIndex,
lastRowIndex: params.lastRowIndex - 1
}
});
if (!skeletonRowsSection) {
return;
}
getRowsParams.start = skeletonRowsSection.firstRowIndex;
getRowsParams.end = skeletonRowsSection.lastRowIndex;
fetchRows(adjustRowParams(getRowsParams));
}, [privateApiRef, adjustRowParams, fetchRows]);
const throttledHandleRenderedRowsIntervalChange = React.useMemo(() => throttle(handleRenderedRowsIntervalChange, props.lazyLoadingRequestThrottleMs), [props.lazyLoadingRequestThrottleMs, handleRenderedRowsIntervalChange]);
React.useEffect(() => {
return () => {
throttledHandleRenderedRowsIntervalChange.clear();
};
}, [throttledHandleRenderedRowsIntervalChange]);
const handleGridSortModelChange = React.useCallback(newSortModel => {
rowsStale.current = true;
throttledHandleRenderedRowsIntervalChange.clear();
previousLastRowIndex.current = 0;
const paginationModel = gridPaginationModelSelector(privateApiRef);
const filterModel = gridFilterModelSelector(privateApiRef);
const getRowsParams = {
start: 0,
end: paginationModel.pageSize - 1,
sortModel: newSortModel,
filterModel
};
privateApiRef.current.setLoading(true);
debouncedFetchRows(getRowsParams);
}, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange]);
const handleGridFilterModelChange = React.useCallback(newFilterModel => {
rowsStale.current = true;
throttledHandleRenderedRowsIntervalChange.clear();
previousLastRowIndex.current = 0;
const paginationModel = gridPaginationModelSelector(privateApiRef);
const sortModel = gridSortModelSelector(privateApiRef);
const getRowsParams = {
start: 0,
end: paginationModel.pageSize - 1,
sortModel,
filterModel: newFilterModel
};
privateApiRef.current.setLoading(true);
debouncedFetchRows(getRowsParams);
}, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange]);
const handleDragStart = React.useCallback(row => {
draggedRowId.current = row.id;
}, []);
const handleDragEnd = React.useCallback(() => {
draggedRowId.current = null;
}, []);
const handleStrategyActivityChange = React.useCallback(() => {
setLazyLoadingRowsUpdateStrategyActive(privateApiRef.current.getActiveStrategy(GridStrategyGroup.DataSource) === DataSourceRowsUpdateStrategy.LazyLoading);
}, [privateApiRef]);
useGridRegisterStrategyProcessor(privateApiRef, DataSourceRowsUpdateStrategy.LazyLoading, 'dataSourceRowsUpdate', handleDataUpdate);
useGridEvent(privateApiRef, 'strategyAvailabilityChange', handleStrategyActivityChange);
useGridEvent(privateApiRef, 'rowCountChange', runIf(lazyLoadingRowsUpdateStrategyActive, handleRowCountChange));
useGridEvent(privateApiRef, 'rowsScrollEndIntersection', runIf(lazyLoadingRowsUpdateStrategyActive, handleIntersection));
useGridEvent(privateApiRef, 'renderedRowsIntervalChange', runIf(lazyLoadingRowsUpdateStrategyActive, throttledHandleRenderedRowsIntervalChange));
useGridEvent(privateApiRef, 'sortModelChange', runIf(lazyLoadingRowsUpdateStrategyActive, handleGridSortModelChange));
useGridEvent(privateApiRef, 'filterModelChange', runIf(lazyLoadingRowsUpdateStrategyActive, handleGridFilterModelChange));
useGridEvent(privateApiRef, 'rowDragStart', runIf(lazyLoadingRowsUpdateStrategyActive, handleDragStart));
useGridEvent(privateApiRef, 'rowDragEnd', runIf(lazyLoadingRowsUpdateStrategyActive, handleDragEnd));
React.useEffect(() => {
setStrategyAvailability();
}, [setStrategyAvailability]);
};