UNPKG

@mui/x-data-grid-pro

Version:

The Pro plan edition of the MUI X Data Grid components.

507 lines (493 loc) 23.3 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useGridDataSourceLazyLoader = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var React = _interopRequireWildcard(require("react")); var _throttle = require("@mui/x-internals/throttle"); var _isDeepEqual = require("@mui/x-internals/isDeepEqual"); var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback")); var _debounce = _interopRequireDefault(require("@mui/utils/debounce")); var _xDataGrid = require("@mui/x-data-grid"); var _internals = require("@mui/x-data-grid/internals"); var _utils = require("../lazyLoader/utils"); var _useGridLazyLoaderPreProcessors = require("../lazyLoader/useGridLazyLoaderPreProcessors"); 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 => `${_useGridLazyLoaderPreProcessors.GRID_SKELETON_ROW_ROOT_ID}-${index}`; /** * @requires useGridRows (state) * @requires useGridPagination (state) * @requires useGridScroll (method */ const useGridDataSourceLazyLoader = (privateApiRef, props) => { const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability(_internals.GridStrategyGroup.DataSource, _internals.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 pollingIntervalRef = React.useRef(null); const fetchRows = React.useCallback(params => { privateApiRef.current.dataSource.fetchRows(_xDataGrid.GRID_ROOT_GROUP_ID, params); }, [privateApiRef]); const debouncedFetchRows = React.useMemo(() => (0, _debounce.default)(fetchRows, 0), [fetchRows]); const revalidate = (0, _useEventCallback.default)(params => { if (rowsStale.current) { return; } // Check cache first — if data is still cached, skip entirely // (no backend call, no diffing needed) const cache = privateApiRef.current.dataSource.cache; const cachedResponse = cache.get(params); if (cachedResponse !== undefined) { return; } // Cache is stale/expired — fetch in background (no loading indicator) debouncedFetchRows(params); }); const stopPolling = React.useCallback(() => { if (pollingIntervalRef.current !== null) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }, []); const startPolling = (0, _useEventCallback.default)(params => { stopPolling(); if (props.dataSourceRevalidateMs <= 0) { return; } pollingIntervalRef.current = setInterval(() => { revalidate(params); }, props.dataSourceRevalidateMs); }); const resetGrid = React.useCallback(() => { privateApiRef.current.setLoading(true); privateApiRef.current.dataSource.cache.clear(); rowsStale.current = true; previousLastRowIndex.current = 0; const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef); const sortModel = (0, _xDataGrid.gridSortModelSelector)(privateApiRef); const filterModel = (0, _xDataGrid.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[_xDataGrid.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[_xDataGrid.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; } let hasChanged = false; // SWR: Only add skeleton padding for never-fetched positions beyond current data. // Previously fetched rows are kept in place (not skeletonized) to avoid flicker on scroll-back. // 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: _xDataGrid.GRID_ROOT_GROUP_ID, depth: 0 }; tree[skeletonId] = skeletonRowNode; hasChanged = true; } } if (!hasChanged) { return; } tree[_xDataGrid.GRID_ROOT_GROUP_ID] = (0, _extends2.default)({}, rootGroup, { children: rootGroupChildren }); privateApiRef.current.setState(state => (0, _extends2.default)({}, state, { rows: (0, _extends2.default)({}, state.rows, { tree }) }), 'addSkeletonRows'); }, [privateApiRef]); 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[_xDataGrid.GRID_ROOT_GROUP_ID]; const rootGroupChildren = [...rootGroup.children]; const filteredSortedRowIds = (0, _xDataGrid.gridFilteredSortedRowIdsSelector)(privateApiRef); const startingIndex = typeof fetchParams.start === 'string' ? Math.max(filteredSortedRowIds.indexOf(fetchParams.start), 0) : fetchParams.start; // Determine if this is a background revalidation (target rows are real, not skeletons) const firstTargetRow = rootGroupChildren[startingIndex]; const isRevalidation = firstTargetRow && tree[firstTargetRow]?.type !== 'skeletonRow'; if (isRevalidation) { // --- SWR PATH --- // Compare response row IDs with existing row IDs at target positions const newRowIds = response.rows.map(row => (0, _xDataGrid.gridRowIdSelector)(privateApiRef, row)); const existingRowIds = rootGroupChildren.slice(startingIndex, startingIndex + response.rows.length); const sameRowIds = existingRowIds.length === newRowIds.length && existingRowIds.every((id, i) => id === newRowIds[i]); if (sameRowIds) { // SAME ROW IDs — check for data changes only const changedRows = response.rows.filter((newRow, i) => { const existingRow = dataRowIdToModelLookup[existingRowIds[i]]; return !(0, _isDeepEqual.isDeepEqual)(newRow, existingRow); }); if (changedRows.length === 0) { // No changes — skip update entirely. Cache already refreshed by fetchRows. privateApiRef.current.setLoading(false); return; } // Efficient data-only update — no tree restructuring needed privateApiRef.current.updateRows(changedRows); // Cache is already updated by fetchRows in useGridDataSourceBase } else { // DIFFERENT ROW IDs — server returned new rows for this range // 1. Remove old rows at target positions for (let i = startingIndex; i < startingIndex + response.rows.length && i < rootGroupChildren.length; i += 1) { const oldRowId = rootGroupChildren[i]; if (oldRowId && tree[oldRowId]?.type !== 'skeletonRow') { delete tree[oldRowId]; delete dataRowIdToModelLookup[oldRowId]; const skeletonId = getSkeletonRowId(i); rootGroupChildren[i] = skeletonId; tree[skeletonId] = { type: 'skeletonRow', id: skeletonId, parent: _xDataGrid.GRID_ROOT_GROUP_ID, depth: 0 }; } } // 2. Duplicate detection for incoming rows let duplicateRowCount = 0; response.rows.forEach(row => { const rowId = (0, _xDataGrid.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: _xDataGrid.GRID_ROOT_GROUP_ID, depth: 0 }; } delete tree[rowId]; delete dataRowIdToModelLookup[rowId]; duplicateRowCount += 1; } }); if (duplicateRowCount > 0) { tree[_xDataGrid.GRID_ROOT_GROUP_ID] = (0, _extends2.default)({}, rootGroup, { children: rootGroupChildren }); privateApiRef.current.setState(state => (0, _extends2.default)({}, state, { rows: (0, _extends2.default)({}, state.rows, { tree, dataRowIdToModelLookup }) })); } // 3. Replace rows privateApiRef.current.unstable_replaceRows(startingIndex, response.rows); } } else { // --- ORIGINAL PATH (skeleton → real row replacement) --- // Check for duplicate rows let duplicateRowCount = 0; response.rows.forEach(row => { const rowId = (0, _xDataGrid.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: _xDataGrid.GRID_ROOT_GROUP_ID, depth: 0 }; } delete tree[rowId]; delete dataRowIdToModelLookup[rowId]; duplicateRowCount += 1; } }); if (duplicateRowCount > 0) { tree[_xDataGrid.GRID_ROOT_GROUP_ID] = (0, _extends2.default)({}, rootGroup, { children: rootGroupChildren }); privateApiRef.current.setState(state => (0, _extends2.default)({}, state, { rows: (0, _extends2.default)({}, 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); if (loadingTrigger.current === LoadingTrigger.VIEWPORT) { startPolling(params.fetchParams); } privateApiRef.current.requestPipeProcessorsApplication('hydrateRows'); }, [privateApiRef, updateLoadingTrigger, addSkeletonRows, startPolling]); 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 = (0, _useEventCallback.default)(() => { if (rowsStale.current || loadingTrigger.current !== LoadingTrigger.SCROLL_END) { return; } const renderContext = (0, _internals.gridRenderContextSelector)(privateApiRef); if (previousLastRowIndex.current >= renderContext.lastRowIndex) { return; } previousLastRowIndex.current = renderContext.lastRowIndex; const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef); const sortModel = (0, _xDataGrid.gridSortModelSelector)(privateApiRef); const filterModel = (0, _xDataGrid.gridFilterModelSelector)(privateApiRef); const getRowsParams = { start: renderContext.lastRowIndex, end: renderContext.lastRowIndex + paginationModel.pageSize - 1, sortModel, filterModel }; privateApiRef.current.setLoading(true); fetchRows((0, _utils.adjustRowParams)(getRowsParams, { pageSize: paginationModel.pageSize, rowCount: privateApiRef.current.state.pagination.rowCount })); }); const handleRenderedRowsIntervalChange = React.useCallback(renderContext => { if (rowsStale.current) { return; } const sortModel = (0, _xDataGrid.gridSortModelSelector)(privateApiRef); const filterModel = (0, _xDataGrid.gridFilterModelSelector)(privateApiRef); const getRowsParams = { start: renderContext.firstRowIndex, end: renderContext.lastRowIndex - 1, sortModel, filterModel }; if (renderedRowsIntervalCache.current.firstRowToRender === renderContext.firstRowIndex && renderedRowsIntervalCache.current.lastRowToRender === renderContext.lastRowIndex) { return; } renderedRowsIntervalCache.current = { firstRowToRender: renderContext.firstRowIndex, lastRowToRender: renderContext.lastRowIndex }; const currentVisibleRows = (0, _internals.getVisibleRows)(privateApiRef); const skeletonRowsSection = (0, _utils.findSkeletonRowsSection)({ apiRef: privateApiRef, visibleRows: currentVisibleRows.rows, range: renderContext }); const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef); if (!skeletonRowsSection) { // SWR: No skeleton rows in viewport — all visible rows have real data. // Schedule background revalidation if cache has expired for this range. if (loadingTrigger.current === LoadingTrigger.VIEWPORT) { const adjustedParams = (0, _utils.adjustRowParams)(getRowsParams, { pageSize: paginationModel.pageSize, rowCount: privateApiRef.current.state.pagination.rowCount }); revalidate(adjustedParams); startPolling(adjustedParams); } return; } getRowsParams.start = skeletonRowsSection.firstRowIndex; getRowsParams.end = skeletonRowsSection.lastRowIndex; fetchRows((0, _utils.adjustRowParams)(getRowsParams, { pageSize: paginationModel.pageSize, rowCount: privateApiRef.current.state.pagination.rowCount })); }, [privateApiRef, fetchRows, revalidate, startPolling]); const throttledHandleRenderedRowsIntervalChange = React.useMemo(() => (0, _throttle.throttle)(handleRenderedRowsIntervalChange, props.lazyLoadingRequestThrottleMs), [props.lazyLoadingRequestThrottleMs, handleRenderedRowsIntervalChange]); React.useEffect(() => { return () => { throttledHandleRenderedRowsIntervalChange.clear(); stopPolling(); }; }, [throttledHandleRenderedRowsIntervalChange, stopPolling]); // Stop polling when dataSourceRevalidateMs is set to 0 React.useEffect(() => { if (props.dataSourceRevalidateMs <= 0) { stopPolling(); } }, [props.dataSourceRevalidateMs, stopPolling]); React.useEffect(() => stopPolling, [stopPolling]); const handleGridSortModelChange = React.useCallback(newSortModel => { rowsStale.current = true; throttledHandleRenderedRowsIntervalChange.clear(); stopPolling(); previousLastRowIndex.current = 0; const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef); const filterModel = (0, _xDataGrid.gridFilterModelSelector)(privateApiRef); const getRowsParams = { start: 0, end: paginationModel.pageSize - 1, sortModel: newSortModel, filterModel }; privateApiRef.current.setLoading(true); debouncedFetchRows(getRowsParams); }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange, stopPolling]); const handleGridFilterModelChange = React.useCallback(newFilterModel => { rowsStale.current = true; throttledHandleRenderedRowsIntervalChange.clear(); stopPolling(); previousLastRowIndex.current = 0; const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef); const sortModel = (0, _xDataGrid.gridSortModelSelector)(privateApiRef); const getRowsParams = { start: 0, end: paginationModel.pageSize - 1, sortModel, filterModel: newFilterModel }; privateApiRef.current.setLoading(true); debouncedFetchRows(getRowsParams); }, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange, stopPolling]); const handleDragStart = React.useCallback(row => { draggedRowId.current = row.id; }, []); const handleDragEnd = React.useCallback(() => { draggedRowId.current = null; }, []); const handleStrategyActivityChange = React.useCallback(() => { setLazyLoadingRowsUpdateStrategyActive(privateApiRef.current.getActiveStrategy(_internals.GridStrategyGroup.DataSource) === _internals.DataSourceRowsUpdateStrategy.LazyLoading); }, [privateApiRef]); // Provide render context based start/end for lazy loading so that // `apiRef.current.dataSource.fetchRows()` without params // re-fetches the currently visible range instead of always using the // pagination-model state. const addGetRowsParams = React.useCallback(params => { if (!lazyLoadingRowsUpdateStrategyActive) { return params; } const renderContext = (0, _internals.gridRenderContextSelector)(privateApiRef); // On initial load the grid hasn't rendered yet — keep the defaults. if (renderContext.lastRowIndex === 0) { return params; } const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef); const adjustedParams = (0, _utils.adjustRowParams)({ start: renderContext.firstRowIndex, end: renderContext.lastRowIndex - 1 }, { pageSize: paginationModel.pageSize, rowCount: privateApiRef.current.state.pagination.rowCount }); return (0, _extends2.default)({}, params, { start: adjustedParams.start, end: adjustedParams.end }); }, [privateApiRef, lazyLoadingRowsUpdateStrategyActive]); (0, _internals.useGridRegisterPipeProcessor)(privateApiRef, 'getRowsParams', addGetRowsParams); (0, _internals.useGridRegisterStrategyProcessor)(privateApiRef, _internals.DataSourceRowsUpdateStrategy.LazyLoading, 'dataSourceRowsUpdate', handleDataUpdate); (0, _xDataGrid.useGridEvent)(privateApiRef, 'strategyAvailabilityChange', handleStrategyActivityChange); (0, _xDataGrid.useGridEvent)(privateApiRef, 'rowCountChange', (0, _internals.runIf)(lazyLoadingRowsUpdateStrategyActive, handleRowCountChange)); (0, _xDataGrid.useGridEvent)(privateApiRef, 'rowsScrollEndIntersection', (0, _internals.runIf)(lazyLoadingRowsUpdateStrategyActive, handleIntersection)); (0, _xDataGrid.useGridEvent)(privateApiRef, 'renderedRowsIntervalChange', (0, _internals.runIf)(lazyLoadingRowsUpdateStrategyActive, throttledHandleRenderedRowsIntervalChange)); (0, _xDataGrid.useGridEvent)(privateApiRef, 'sortModelChange', (0, _internals.runIf)(lazyLoadingRowsUpdateStrategyActive, handleGridSortModelChange)); (0, _xDataGrid.useGridEvent)(privateApiRef, 'filterModelChange', (0, _internals.runIf)(lazyLoadingRowsUpdateStrategyActive, handleGridFilterModelChange)); (0, _xDataGrid.useGridEvent)(privateApiRef, 'rowDragStart', (0, _internals.runIf)(lazyLoadingRowsUpdateStrategyActive, handleDragStart)); (0, _xDataGrid.useGridEvent)(privateApiRef, 'rowDragEnd', (0, _internals.runIf)(lazyLoadingRowsUpdateStrategyActive, handleDragEnd)); React.useEffect(() => { setStrategyAvailability(); }, [setStrategyAvailability]); }; exports.useGridDataSourceLazyLoader = useGridDataSourceLazyLoader;