From 659dc5f42d3ba485c6dfdb7ae52f5e899bc66417 Mon Sep 17 00:00:00 2001 From: Armin Mehinovic <4390250+arminmeh@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:36:51 +0100 Subject: [PATCH] [DataGridPro] Server side data source lazy loading (#13878) Signed-off-by: Armin Mehinovic <4390250+arminmeh@users.noreply.github.com> Co-authored-by: Kenan Yusuf Co-authored-by: Bilal Shafi Co-authored-by: Sycamore <71297412+samuelsycamore@users.noreply.github.com> --- docs/data/data-grid/events/events.json | 2 +- .../ServerSideDataGridNoCache.js | 1 + .../ServerSideDataGridNoCache.tsx | 1 + .../ServerSideLazyLoadingErrorHandling.js | 109 ++++ .../ServerSideLazyLoadingErrorHandling.tsx | 113 ++++ .../ServerSideLazyLoadingInfinite.js | 44 ++ .../ServerSideLazyLoadingInfinite.tsx | 48 ++ .../ServerSideLazyLoadingModeUpdate.js | 82 +++ .../ServerSideLazyLoadingModeUpdate.tsx | 94 ++++ .../ServerSideLazyLoadingRequestThrottle.js | 114 ++++ .../ServerSideLazyLoadingRequestThrottle.tsx | 133 +++++ .../ServerSideLazyLoadingViewport.js | 45 ++ .../ServerSideLazyLoadingViewport.tsx | 49 ++ docs/data/data-grid/server-side-data/index.md | 43 +- .../server-side-data/infinite-loading.md | 15 - .../server-side-data/lazy-loading.md | 114 +++- .../data-grid/server-side-data/tree-data.md | 6 +- docs/data/pages.ts | 7 +- .../x/api/data-grid/data-grid-premium.json | 5 +- docs/pages/x/api/data-grid/data-grid-pro.json | 5 +- .../server-side-data/infinite-loading.js | 7 - .../data-grid-premium/data-grid-premium.json | 10 +- .../data-grid-pro/data-grid-pro.json | 10 +- .../src/hooks/serverUtils.ts | 25 +- .../src/hooks/useMockServer.ts | 5 +- .../src/DataGridPremium/DataGridPremium.tsx | 15 +- .../useDataGridPremiumComponent.tsx | 2 + .../rowGrouping/gridRowGroupingUtils.ts | 3 +- .../rowGrouping/useGridRowGrouping.tsx | 5 +- .../src/DataGridPro/DataGridPro.tsx | 15 +- .../DataGridPro/useDataGridProComponent.tsx | 2 + .../src/DataGridPro/useDataGridProProps.ts | 2 + .../src/hooks/features/dataSource/cache.ts | 7 +- .../hooks/features/dataSource/interfaces.ts | 12 +- .../features/dataSource/useGridDataSource.ts | 206 +++++-- .../src/hooks/features/dataSource/utils.ts | 93 +++- .../src/hooks/features/index.ts | 2 +- .../features/lazyLoader/useGridLazyLoader.ts | 46 +- .../src/hooks/features/lazyLoader/utils.ts | 46 ++ .../useGridDataSourceLazyLoader.ts | 512 ++++++++++++++++++ ...useGridDataSourceTreeDataPreProcessors.tsx | 3 +- .../treeData/useGridTreeDataPreProcessors.tsx | 3 +- .../x-data-grid-pro/src/internals/index.ts | 1 + .../src/internals/propValidation.ts | 6 + .../src/models/dataGridProProps.ts | 15 +- .../dataSourceLazyLoader.DataGridPro.test.tsx | 337 ++++++++++++ .../src/typeOverloads/modules.ts | 1 + .../gridStrategyProcessingApi.ts | 38 +- .../useGridStrategyProcessing.ts | 20 +- .../src/hooks/features/rows/useGridRows.ts | 20 +- packages/x-data-grid/src/internals/index.ts | 1 + .../src/models/events/gridEventLookup.ts | 1 + .../x-data-grid/src/models/gridDataSource.ts | 4 +- 53 files changed, 2314 insertions(+), 191 deletions(-) create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingErrorHandling.js create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingErrorHandling.tsx create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingInfinite.js create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingInfinite.tsx create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingModeUpdate.js create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingModeUpdate.tsx create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingRequestThrottle.js create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingRequestThrottle.tsx create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingViewport.js create mode 100644 docs/data/data-grid/server-side-data/ServerSideLazyLoadingViewport.tsx delete mode 100644 docs/data/data-grid/server-side-data/infinite-loading.md delete mode 100644 docs/pages/x/react-data-grid/server-side-data/infinite-loading.js create mode 100644 packages/x-data-grid-pro/src/hooks/features/lazyLoader/utils.ts create mode 100644 packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts create mode 100644 packages/x-data-grid-pro/src/tests/dataSourceLazyLoader.DataGridPro.test.tsx diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json index a2831d2974c2..fedd745dd5c4 100644 --- a/docs/data/data-grid/events/events.json +++ b/docs/data/data-grid/events/events.json @@ -227,7 +227,7 @@ { "projects": ["x-data-grid-pro", "x-data-grid-premium"], "name": "fetchRows", - "description": "Fired when a new batch of rows is requested to be loaded. Called with a GridFetchRowsParams object.", + "description": "Fired when a new batch of rows is requested to be loaded. Called with a GridFetchRowsParams object. Used to trigger onFetchRows.", "params": "GridFetchRowsParams", "event": "MuiEvent<{}>", "componentProp": "onFetchRows" diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridNoCache.js b/docs/data/data-grid/server-side-data/ServerSideDataGridNoCache.js index 43c742a5b90a..e598eb38982e 100644 --- a/docs/data/data-grid/server-side-data/ServerSideDataGridNoCache.js +++ b/docs/data/data-grid/server-side-data/ServerSideDataGridNoCache.js @@ -38,6 +38,7 @@ export default function ServerSideDataGridNoCache() { ...initialState, pagination: { paginationModel: { pageSize: 10, page: 0 }, + rowCount: 0, }, }), [initialState], diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridNoCache.tsx b/docs/data/data-grid/server-side-data/ServerSideDataGridNoCache.tsx index b62606d8985f..19a578b73a8c 100644 --- a/docs/data/data-grid/server-side-data/ServerSideDataGridNoCache.tsx +++ b/docs/data/data-grid/server-side-data/ServerSideDataGridNoCache.tsx @@ -38,6 +38,7 @@ export default function ServerSideDataGridNoCache() { ...initialState, pagination: { paginationModel: { pageSize: 10, page: 0 }, + rowCount: 0, }, }), [initialState], diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingErrorHandling.js b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingErrorHandling.js new file mode 100644 index 000000000000..4f890e86fe5e --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingErrorHandling.js @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { + DataGridPro, + useGridApiRef, + GridToolbar, + GRID_ROOT_GROUP_ID, +} from '@mui/x-data-grid-pro'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; +import Snackbar from '@mui/material/Snackbar'; + +function ErrorSnackbar(props) { + const { onRetry, ...rest } = props; + return ( + + + Retry + + } + > + Failed to fetch row data + + + ); +} + +function ServerSideLazyLoadingErrorHandling() { + const apiRef = useGridApiRef(); + const [retryParams, setRetryParams] = React.useState(null); + const [shouldRequestsFail, setShouldRequestsFail] = React.useState(false); + + const { fetchRows, ...props } = useMockServer( + { rowLength: 100 }, + { useCursorPagination: false, minDelay: 300, maxDelay: 800 }, + shouldRequestsFail, + ); + + const dataSource = React.useMemo( + () => ({ + getRows: async (params) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + // Reset the retryParams when new rows are fetched + setRetryParams(null); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + }), + [fetchRows], + ); + + return ( +
+ setShouldRequestsFail(event.target.checked)} + /> + } + label="Make the requests fail" + /> +
+ {retryParams && ( + { + apiRef.current.unstable_dataSource.fetchRows( + GRID_ROOT_GROUP_ID, + retryParams, + ); + setRetryParams(null); + }} + /> + )} + setRetryParams(params)} + unstable_dataSourceCache={null} + unstable_lazyLoading + paginationModel={{ page: 0, pageSize: 10 }} + slots={{ toolbar: GridToolbar }} + /> +
+
+ ); +} + +export default ServerSideLazyLoadingErrorHandling; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingErrorHandling.tsx b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingErrorHandling.tsx new file mode 100644 index 000000000000..53d4519d1fa2 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingErrorHandling.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { + DataGridPro, + useGridApiRef, + GridToolbar, + GridDataSource, + GridGetRowsParams, + GRID_ROOT_GROUP_ID, +} from '@mui/x-data-grid-pro'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; +import Snackbar, { SnackbarProps } from '@mui/material/Snackbar'; + +function ErrorSnackbar(props: SnackbarProps & { onRetry: () => void }) { + const { onRetry, ...rest } = props; + return ( + + + Retry + + } + > + Failed to fetch row data + + + ); +} + +function ServerSideLazyLoadingErrorHandling() { + const apiRef = useGridApiRef(); + const [retryParams, setRetryParams] = React.useState( + null, + ); + const [shouldRequestsFail, setShouldRequestsFail] = React.useState(false); + + const { fetchRows, ...props } = useMockServer( + { rowLength: 100 }, + { useCursorPagination: false, minDelay: 300, maxDelay: 800 }, + shouldRequestsFail, + ); + + const dataSource: GridDataSource = React.useMemo( + () => ({ + getRows: async (params) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + // Reset the retryParams when new rows are fetched + setRetryParams(null); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + }), + [fetchRows], + ); + + return ( +
+ setShouldRequestsFail(event.target.checked)} + /> + } + label="Make the requests fail" + /> +
+ {retryParams && ( + { + apiRef.current.unstable_dataSource.fetchRows( + GRID_ROOT_GROUP_ID, + retryParams, + ); + setRetryParams(null); + }} + /> + )} + setRetryParams(params)} + unstable_dataSourceCache={null} + unstable_lazyLoading + paginationModel={{ page: 0, pageSize: 10 }} + slots={{ toolbar: GridToolbar }} + /> +
+
+ ); +} + +export default ServerSideLazyLoadingErrorHandling; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingInfinite.js b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingInfinite.js new file mode 100644 index 000000000000..59ff744f4e43 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingInfinite.js @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { DataGridPro } from '@mui/x-data-grid-pro'; +import { useMockServer } from '@mui/x-data-grid-generator'; + +function ServerSideLazyLoadingInfinite() { + const { fetchRows, ...props } = useMockServer( + { rowLength: 100 }, + { useCursorPagination: false, minDelay: 200, maxDelay: 500 }, + ); + + const dataSource = React.useMemo( + () => ({ + getRows: async (params) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + return { + rows: getRowsResponse.rows, + }; + }, + }), + [fetchRows], + ); + + return ( +
+ +
+ ); +} + +export default ServerSideLazyLoadingInfinite; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingInfinite.tsx b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingInfinite.tsx new file mode 100644 index 000000000000..b842453551b7 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingInfinite.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { + DataGridPro, + GridDataSource, + GridGetRowsParams, +} from '@mui/x-data-grid-pro'; +import { useMockServer } from '@mui/x-data-grid-generator'; + +function ServerSideLazyLoadingInfinite() { + const { fetchRows, ...props } = useMockServer( + { rowLength: 100 }, + { useCursorPagination: false, minDelay: 200, maxDelay: 500 }, + ); + + const dataSource: GridDataSource = React.useMemo( + () => ({ + getRows: async (params: GridGetRowsParams) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + return { + rows: getRowsResponse.rows, + }; + }, + }), + [fetchRows], + ); + + return ( +
+ +
+ ); +} + +export default ServerSideLazyLoadingInfinite; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingModeUpdate.js b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingModeUpdate.js new file mode 100644 index 000000000000..ca6447d8e7e0 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingModeUpdate.js @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { DataGridPro } from '@mui/x-data-grid-pro'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Radio from '@mui/material/Radio'; + +function GridCustomToolbar({ count, setCount }) { + return ( + + Row count + setCount(Number(event.target.value))} + > + } label="Unknown" /> + } label="40" /> + } label="100" /> + + + ); +} + +function ServerSideLazyLoadingModeUpdate() { + const { fetchRows, ...props } = useMockServer( + { rowLength: 100 }, + { useCursorPagination: false, minDelay: 200, maxDelay: 500 }, + ); + + const [rowCount, setRowCount] = React.useState(-1); + + const dataSource = React.useMemo( + () => ({ + getRows: async (params) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + return { + rows: getRowsResponse.rows, + }; + }, + }), + [fetchRows], + ); + + return ( +
+ +
+ ); +} + +export default ServerSideLazyLoadingModeUpdate; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingModeUpdate.tsx b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingModeUpdate.tsx new file mode 100644 index 000000000000..13ec706795e0 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingModeUpdate.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { + DataGridPro, + GridDataSource, + GridGetRowsParams, + GridSlotProps, +} from '@mui/x-data-grid-pro'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Radio from '@mui/material/Radio'; + +declare module '@mui/x-data-grid' { + interface ToolbarPropsOverrides { + count: number; + setCount: React.Dispatch>; + } +} + +function GridCustomToolbar({ count, setCount }: GridSlotProps['toolbar']) { + return ( + + Row count + setCount(Number(event.target.value))} + > + } label="Unknown" /> + } label="40" /> + } label="100" /> + + + ); +} + +function ServerSideLazyLoadingModeUpdate() { + const { fetchRows, ...props } = useMockServer( + { rowLength: 100 }, + { useCursorPagination: false, minDelay: 200, maxDelay: 500 }, + ); + + const [rowCount, setRowCount] = React.useState(-1); + + const dataSource: GridDataSource = React.useMemo( + () => ({ + getRows: async (params: GridGetRowsParams) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + return { + rows: getRowsResponse.rows, + }; + }, + }), + [fetchRows], + ); + + return ( +
+ +
+ ); +} + +export default ServerSideLazyLoadingModeUpdate; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingRequestThrottle.js b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingRequestThrottle.js new file mode 100644 index 000000000000..2a826d5dc52a --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingRequestThrottle.js @@ -0,0 +1,114 @@ +import * as React from 'react'; +import { DataGridPro, GridRowCount } from '@mui/x-data-grid-pro'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Box from '@mui/material/Box'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormLabel from '@mui/material/FormLabel'; +import RadioGroup from '@mui/material/RadioGroup'; +import Radio from '@mui/material/Radio'; + +function GridCustomFooterRowCount({ requestCount, ...props }) { + return ( + + Request count: {requestCount} + + + ); +} + +function GridCustomToolbar({ throttleMs, setThrottleMs }) { + return ( + + + + Throttle + + setThrottleMs(Number(event.target.value))} + > + } label="0 ms" /> + } + label="500 ms (default)" + /> + } label="1500 ms" /> + + + + ); +} + +function ServerSideLazyLoadingRequestThrottle() { + const rowCount = 1000; + const { fetchRows, ...props } = useMockServer( + { rowLength: rowCount }, + { useCursorPagination: false, minDelay: 200, maxDelay: 500 }, + ); + + const [requestCount, setRequestCount] = React.useState(0); + const [throttleMs, setThrottleMs] = React.useState(500); + + const dataSource = React.useMemo( + () => ({ + getRows: async (params) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + setRequestCount((prev) => prev + 1); + return { + rows: getRowsResponse.rows, + }; + }, + }), + [fetchRows], + ); + + React.useEffect(() => { + setRequestCount(0); + }, [dataSource]); + + return ( +
+ +
+ ); +} + +export default ServerSideLazyLoadingRequestThrottle; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingRequestThrottle.tsx b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingRequestThrottle.tsx new file mode 100644 index 000000000000..75056075affc --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingRequestThrottle.tsx @@ -0,0 +1,133 @@ +import * as React from 'react'; +import { + DataGridPro, + GridDataSource, + GridGetRowsParams, + GridRowCount, + GridSlotProps, +} from '@mui/x-data-grid-pro'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Box from '@mui/material/Box'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormLabel from '@mui/material/FormLabel'; +import RadioGroup from '@mui/material/RadioGroup'; +import Radio from '@mui/material/Radio'; + +declare module '@mui/x-data-grid' { + interface ToolbarPropsOverrides { + throttleMs: number; + setThrottleMs: React.Dispatch>; + } + interface FooterRowCountOverrides { + requestCount?: number; + } +} + +function GridCustomFooterRowCount({ + requestCount, + ...props +}: GridSlotProps['footerRowCount']) { + return ( + + Request count: {requestCount} + + + ); +} + +function GridCustomToolbar({ throttleMs, setThrottleMs }: GridSlotProps['toolbar']) { + return ( + + + + Throttle + + setThrottleMs(Number(event.target.value))} + > + } label="0 ms" /> + } + label="500 ms (default)" + /> + } label="1500 ms" /> + + + + ); +} + +function ServerSideLazyLoadingRequestThrottle() { + const rowCount = 1000; + const { fetchRows, ...props } = useMockServer( + { rowLength: rowCount }, + { useCursorPagination: false, minDelay: 200, maxDelay: 500 }, + ); + + const [requestCount, setRequestCount] = React.useState(0); + const [throttleMs, setThrottleMs] = React.useState(500); + + const dataSource: GridDataSource = React.useMemo( + () => ({ + getRows: async (params: GridGetRowsParams) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + setRequestCount((prev) => prev + 1); + return { + rows: getRowsResponse.rows, + }; + }, + }), + [fetchRows], + ); + + React.useEffect(() => { + setRequestCount(0); + }, [dataSource]); + + return ( +
+ +
+ ); +} + +export default ServerSideLazyLoadingRequestThrottle; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingViewport.js b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingViewport.js new file mode 100644 index 000000000000..d6de6e60ca0d --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingViewport.js @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { DataGridPro } from '@mui/x-data-grid-pro'; +import { useMockServer } from '@mui/x-data-grid-generator'; + +function ServerSideLazyLoadingViewport() { + const { fetchRows, ...props } = useMockServer( + { rowLength: 100000 }, + { useCursorPagination: false, minDelay: 200, maxDelay: 500 }, + ); + + const dataSource = React.useMemo( + () => ({ + getRows: async (params) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + }), + [fetchRows], + ); + + return ( +
+ +
+ ); +} + +export default ServerSideLazyLoadingViewport; diff --git a/docs/data/data-grid/server-side-data/ServerSideLazyLoadingViewport.tsx b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingViewport.tsx new file mode 100644 index 000000000000..df60d3b0e3dc --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideLazyLoadingViewport.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { + DataGridPro, + GridDataSource, + GridGetRowsParams, +} from '@mui/x-data-grid-pro'; +import { useMockServer } from '@mui/x-data-grid-generator'; + +function ServerSideLazyLoadingViewport() { + const { fetchRows, ...props } = useMockServer( + { rowLength: 100000 }, + { useCursorPagination: false, minDelay: 200, maxDelay: 500 }, + ); + + const dataSource: GridDataSource = React.useMemo( + () => ({ + getRows: async (params: GridGetRowsParams) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + }), + [fetchRows], + ); + + return ( +
+ +
+ ); +} + +export default ServerSideLazyLoadingViewport; diff --git a/docs/data/data-grid/server-side-data/index.md b/docs/data/data-grid/server-side-data/index.md index 035be2e8fdf6..02906265e955 100644 --- a/docs/data/data-grid/server-side-data/index.md +++ b/docs/data/data-grid/server-side-data/index.md @@ -176,10 +176,10 @@ The following demo showcases this behavior. {{"demo": "ServerSideDataGrid.js", "bg": "inline"}} :::info -The data source demos use a utility function `useMockServer` to simulate the server-side data fetching. -In a real-world scenario, you should replace this with your own server-side data-fetching logic. +The data source demos use a `useMockServer` utility function to simulate server-side data fetching. +In a real-world scenario you would replace this with your own server-side data-fetching logic. -Open info section of the browser console to see the requests being made and the data being fetched in response. +Open the Info section of your browser console to see the requests being made and the data being fetched in response. ::: ## Data caching @@ -189,6 +189,43 @@ This means that if the user navigates to a page or expands a node that has alrea The `GridDataSourceCacheDefault` is used by default which is a simple in-memory cache that stores the data in a plain object. It can be seen in action in the [demo above](#with-data-source). +### Improving the cache hit rate + +To increase the cache hit rate, Data Grid splits `getRows()` results into chunks before storing them in cache. +For the requests that follow, chunks are combined as needed to recreate the response. +This means that a single request can make multiple calls to the `get()` or `set()` method of `GridDataSourceCache`. + +Chunk size is the lowest expected amount of records per request based on the `pageSize` value from the `paginationModel` and `pageSizeOptions` props. + +Because of this, values in the `pageSizeOptions` prop play a big role in the cache hit rate. +We recommend using values that are multiples of the lowest value; even better if each subsequent value is a multiple of the previous value. + +Here are some examples: + +1. Best scenario - `pageSizeOptions={[5, 10, 50, 100]}` + + In this case the chunk size is 5, which means that with `pageSize={100}` there are 20 cache records stored. + + Retrieving data for any other `pageSize` up to the first 100 records results in a cache hit, since the whole dataset can be made of the existing chunks. + +2. Parts of the data missing - `pageSizeOptions={[10, 20, 50]}` + + Loading the first page with `pageSize={50}` results in 5 cache records. + This works well with `pageSize={10}`, but not as well with `pageSize={20}`. + Loading the third page with `pageSize={20}` results in a new request being made, even though half of the data is already in the cache. + +3. Incompatible page sizes - `pageSizeOptions={[7, 15, 40]}` + + In this situation, the chunk size is 7. + Retrieving the first page with `pageSize={15}` creates chunks split into `[7, 7, 1]` records. + Loading the second page creates 3 new chunks (again `[7, 7, 1]`), but now the third chunk from the first request has an overlap of 1 record with the first chunk of the second request. + These chunks with 1 record can only be used as the last piece of a request for `pageSize={15}` and are useless in all other cases. + +:::info +In the examples above, `sortModel` and `filterModel` remained unchanged. +Changing those would require a new response to be retrieved and stored in the chunks. +::: + ### Customize the cache lifetime The `GridDataSourceCacheDefault` has a default Time To Live (`ttl`) of 5 minutes. To customize it, pass the `ttl` option in milliseconds to the `GridDataSourceCacheDefault` constructor, and then pass it as the `unstable_dataSourceCache` prop. diff --git a/docs/data/data-grid/server-side-data/infinite-loading.md b/docs/data/data-grid/server-side-data/infinite-loading.md deleted file mode 100644 index ae4b15c5dffb..000000000000 --- a/docs/data/data-grid/server-side-data/infinite-loading.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: React Data Grid - Server-side infinite loading ---- - -# Data Grid - Server-side infinite loading [](/x/introduction/licensing/#pro-plan 'Pro plan')🚧 - -

Row infinite loading with server-side data source.

- -:::warning -This feature isn't implemented yet. It's coming. - -👍 Upvote [issue #10858](https://github.com/mui/mui-x/issues/10858) if you want to see it land faster. - -Don't hesitate to leave a comment on the same issue to influence what gets built. Especially if you already have a use case for this component, or if you are facing a pain point with the [current solution](https://mui.com/x/react-data-grid/row-updates/#infinite-loading). -::: diff --git a/docs/data/data-grid/server-side-data/lazy-loading.md b/docs/data/data-grid/server-side-data/lazy-loading.md index 123fb3d6733c..176ddeff2913 100644 --- a/docs/data/data-grid/server-side-data/lazy-loading.md +++ b/docs/data/data-grid/server-side-data/lazy-loading.md @@ -2,14 +2,120 @@ title: React Data Grid - Server-side lazy loading --- -# Data Grid - Server-side lazy loading [](/x/introduction/licensing/#pro-plan 'Pro plan')🚧 +# Data Grid - Server-side lazy loading [](/x/introduction/licensing/#pro-plan 'Pro plan')🧪 -

Row lazy-loading with server-side data source.

+

Learn how to implement lazy-loading rows with a server-side data source.

+ +Lazy loading changes the way pagination works by removing page controls and loading data dynamically (in a single list) as the user scrolls through the grid. + +You can enable it with the `unstable_lazyLoading` prop paired with the `unstable_dataSource` prop. + +Initially, data for the first page is fetched and displayed in the grid. +The value of the total row count determines when the next page's data is loaded: + +- If the total row count is known, the Data Grid is filled with skeleton rows and fetches more data if one of the skeleton rows falls into the rendering context. + This loading strategy is often referred to as [**viewport loading**](#viewport-loading). + +- If the total row count is unknown, the Data Grid fetches more data when the user scrolls to the bottom. + This loading strategy is often referred to as [**infinite loading**](#infinite-loading). + +:::info +You can provide the row count through one of the following ways: + +- Pass it as the [`rowCount`](/x/api/data-grid/data-grid/#data-grid-prop-rowCount) prop +- Return `rowCount` in the `getRows()` method of the [data source](/x/react-data-grid/server-side-data/#data-source) +- Set the `rowCount` using the [`setRowCount()`](/x/api/data-grid/grid-api/#grid-api-prop-setRowCount) API method + +These options are presented in order of precedence, which means if the row count is set using the API, that value is overridden once a new value is returned by the `getRows()` method unless it's `undefined`. +::: + +## Viewport loading + +Viewport loading mode is enabled when the row count is known (and is greater than or equal to zero). +The Grid fetches the first page immediately and adds skeleton rows to match the total row count. +Other pages are fetched once the user starts scrolling and moves a skeleton row inside the rendering context (with the index range defined by [virtualization](/x/react-data-grid/virtualization/)). + +If the user scrolls too fast, the Grid loads multiple pages with one request (by adjusting `start` and `end` parameters) to reduce the server load. + +The demo below shows how viewport loading mode works: + +{{"demo": "ServerSideLazyLoadingViewport.js", "bg": "inline"}} + +:::info +The data source demos use a `useMockServer` utility function to simulate server-side data fetching. +In a real-world scenario you would replace this with your own server-side data-fetching logic. + +Open the Info section of your browser console to see the requests being made and the data being fetched in response. +::: + +### Request throttling + +As a user scrolls through the Grid, the rendering context changes and the Grid tries to fill in any missing rows by making a new server request. +It also throttles new data fetches to avoid making unnecessary requests. +The default throttle time is 500 milliseconds. +Use the `unstable_lazyLoadingRequestThrottleMs` prop to set a custom time, as shown below: + +{{"demo": "ServerSideLazyLoadingRequestThrottle.js", "bg": "inline"}} + +## Infinite loading + +Infinite loading mode is enabled when the row count is unknown (either `-1` or `undefined`). +A new page is loaded when the scroll reaches the bottom of the viewport area. + +You can use the `scrollEndThreshold` prop to change the area that triggers new requests. + +The demo below shows how infinite loading mode works. +Page size is set to `15` and the mock server is configured to return a total of 100 rows. +When the response contains no new rows, the Grid stops requesting new data. + +{{"demo": "ServerSideLazyLoadingInfinite.js", "bg": "inline"}} + +## Updating the loading mode + +The Grid changes the loading mode dynamically if the total row count gets updated by changing the `rowCount` prop, returning different `rowCount` in `GridGetRowsResponse` or via `setRowCount()` API. + +Based on the previous and the new value for the total row count, the following scenarios are possible: + +- **Unknown `rowCount` to known `rowCount`**: When the row count is set to a valid value from an unknown value, the Data Grid switches to viewport loading mode. It checks the number of already fetched rows and adds skeleton rows to match the provided row count. + +- **Known `rowCount` to unknown `rowCount`**: If the row count is updated and set to `-1`, the Data Grid resets, fetches the first page, then sets itself to infinite loading mode. + +- **Known `rowCount` greater than the actual row count**: This can happen either by reducing the value of the row count after more rows were already fetched, or if the row count was unknown and the Grid (while in the infinite loading mode) already fetched more rows. In this case, the Grid resets, fetches the first page, and then continues in one mode or the other depending on the new value of the `rowCount`. + +:::warning +`rowCount` is expected to be static. +Changing its value can cause the Grid to reset and the cache to be cleared which may lead to performance and UX degradation. +::: + +The demo below serves as a showcase of the behavior described above, and is not representative of something you would implement in a real-world scenario. + +{{"demo": "ServerSideLazyLoadingModeUpdate.js", "bg": "inline"}} + +## Nested lazy loading 🚧 :::warning This feature isn't implemented yet. It's coming. -👍 Upvote [issue #10857](https://github.com/mui/mui-x/issues/10857) if you want to see it land faster. +👍 Upvote [issue #14527](https://github.com/mui/mui-x/issues/14527) if you want to see it land faster. -Don't hesitate to leave a comment on the same issue to influence what gets built. Especially if you already have a use case for this component, or if you are facing a pain point with the [current solution](https://mui.com/x/react-data-grid/row-updates/#lazy-loading). +Don't hesitate to leave a comment on the issue to help influence what gets built—especially if you already have a use case for this feature, or if you're facing a specific pain point with your current solution. ::: + +When completed, it will be possible to use the `unstable_lazyLoading` flag in combination with [tree data](/x/react-data-grid/server-side-data/tree-data/) and [row grouping](/x/react-data-grid/server-side-data/row-grouping/). + +## Error handling + +To handle errors, use the `unstable_onDataSourceError()` prop as described in [Server-side data—Error handling](/x/react-data-grid/server-side-data/#error-handling). + +You can pass the second parameter of type `GridGetRowsParams` to the `getRows()` method of the [`unstable_dataSource`](/x/api/data-grid/grid-api/#grid-api-prop-unstable_dataSource) to retry the request. +If successful, the Data Grid uses `rows` and `rowCount` data to determine if the rows should be appended at the end of the grid or if the skeleton rows should be replaced. + +The following demo gives an example how to use `GridGetRowsParams` to retry a failed request. + +{{"demo": "ServerSideLazyLoadingErrorHandling.js", "bg": "inline"}} + +## API + +- [DataGrid](/x/api/data-grid/data-grid/) +- [DataGridPro](/x/api/data-grid/data-grid-pro/) +- [DataGridPremium](/x/api/data-grid/data-grid-premium/) diff --git a/docs/data/data-grid/server-side-data/tree-data.md b/docs/data/data-grid/server-side-data/tree-data.md index cc0d15854c58..b4af9c354839 100644 --- a/docs/data/data-grid/server-side-data/tree-data.md +++ b/docs/data/data-grid/server-side-data/tree-data.md @@ -59,10 +59,10 @@ It also caches the data by default. {{"demo": "ServerSideTreeData.js", "bg": "inline"}} :::info -The data source demos use a utility function `useMockServer` to simulate the server-side data fetching. -In a real-world scenario, you would replace this with your own server-side data fetching logic. +The data source demos use a `useMockServer` utility function to simulate server-side data fetching. +In a real-world scenario you would replace this with your own server-side data-fetching logic. -Open the info section of the browser console to see the requests being made and the data being fetched in response. +Open the Info section of your browser console to see the requests being made and the data being fetched in response. ::: ## Error handling diff --git a/docs/data/pages.ts b/docs/data/pages.ts index 1dac291fa82d..ffd848c5fa46 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -154,12 +154,7 @@ const pages: MuiPage[] = [ { pathname: '/x/react-data-grid/server-side-data/lazy-loading', plan: 'pro', - planned: true, - }, - { - pathname: '/x/react-data-grid/server-side-data/infinite-loading', - plan: 'pro', - planned: true, + unstable: true, }, { pathname: '/x/react-data-grid/server-side-data/row-grouping', diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index 5663a6235bb1..c19a664541cd 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -603,7 +603,8 @@ "default": "{ parents: true, descendants: true }" }, "rowsLoadingMode": { - "type": { "name": "enum", "description": "'client'
| 'server'" } + "type": { "name": "enum", "description": "'client'
| 'server'" }, + "default": "\"client\"" }, "rowSpacingType": { "type": { "name": "enum", "description": "'border'
| 'margin'" }, @@ -642,6 +643,8 @@ }, "throttleRowsMs": { "type": { "name": "number" }, "default": "0" }, "treeData": { "type": { "name": "bool" }, "default": "false" }, + "unstable_lazyLoading": { "type": { "name": "bool" }, "default": "false" }, + "unstable_lazyLoadingRequestThrottleMs": { "type": { "name": "number" }, "default": "500" }, "unstable_listColumn": { "type": { "name": "shape", diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index bc21892c67b0..677e8939b095 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -541,7 +541,8 @@ "default": "{ parents: true, descendants: true }" }, "rowsLoadingMode": { - "type": { "name": "enum", "description": "'client'
| 'server'" } + "type": { "name": "enum", "description": "'client'
| 'server'" }, + "default": "\"client\"" }, "rowSpacingType": { "type": { "name": "enum", "description": "'border'
| 'margin'" }, @@ -576,6 +577,8 @@ }, "throttleRowsMs": { "type": { "name": "number" }, "default": "0" }, "treeData": { "type": { "name": "bool" }, "default": "false" }, + "unstable_lazyLoading": { "type": { "name": "bool" }, "default": "false" }, + "unstable_lazyLoadingRequestThrottleMs": { "type": { "name": "number" }, "default": "500" }, "unstable_listColumn": { "type": { "name": "shape", diff --git a/docs/pages/x/react-data-grid/server-side-data/infinite-loading.js b/docs/pages/x/react-data-grid/server-side-data/infinite-loading.js deleted file mode 100644 index 8392e3a0416f..000000000000 --- a/docs/pages/x/react-data-grid/server-side-data/infinite-loading.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; -import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; -import * as pageProps from 'docsx/data/data-grid/server-side-data/infinite-loading.md?muiMarkdown'; - -export default function Page() { - return ; -} diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index 31694d362767..65776cfbe2bb 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -619,7 +619,7 @@ "description": "When rowSelectionPropagation.descendants is set to true. - Selecting a parent selects all its filtered descendants automatically. - Deselecting a parent row deselects all its filtered descendants automatically.
When rowSelectionPropagation.parents is set to true - Selecting all the filtered descendants of a parent selects the parent automatically. - Deselecting a descendant of a selected parent deselects the parent automatically.
Works with tree data and row grouping on the client-side only." }, "rowsLoadingMode": { - "description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"" + "description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading." }, "rowSpacingType": { "description": "Sets the type of space between rows added by getRowSpacing." @@ -628,7 +628,7 @@ "description": "Override the height/width of the Data Grid inner scrollbar." }, "scrollEndThreshold": { - "description": "Set the area in px at the bottom of the grid viewport where onRowsScrollEnd is called." + "description": "Set the area in px at the bottom of the grid viewport where onRowsScrollEnd is called. If combined with unstable_lazyLoading, it defines the area where the next data request is triggered." }, "showCellVerticalBorder": { "description": "If true, vertical borders will be displayed between cells." @@ -658,6 +658,12 @@ "treeData": { "description": "If true, the rows will be gathered in a tree structure according to the getTreeDataPath prop." }, + "unstable_lazyLoading": { + "description": "Used together with unstable_dataSource to enable lazy loading. If enabled, the grid stops adding paginationModel to the data requests (getRows) and starts sending start and end values depending on the loading mode and the scroll position." + }, + "unstable_lazyLoadingRequestThrottleMs": { + "description": "If positive, the Data Grid will throttle data source requests on rendered rows interval change." + }, "unstable_listColumn": { "description": "Definition of the column rendered when the unstable_listView prop is enabled." }, diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index dbac4fd0c9af..ee17a6dd230c 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -561,7 +561,7 @@ "description": "When rowSelectionPropagation.descendants is set to true. - Selecting a parent selects all its filtered descendants automatically. - Deselecting a parent row deselects all its filtered descendants automatically.
When rowSelectionPropagation.parents is set to true - Selecting all the filtered descendants of a parent selects the parent automatically. - Deselecting a descendant of a selected parent deselects the parent automatically.
Works with tree data and row grouping on the client-side only." }, "rowsLoadingMode": { - "description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"" + "description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading." }, "rowSpacingType": { "description": "Sets the type of space between rows added by getRowSpacing." @@ -570,7 +570,7 @@ "description": "Override the height/width of the Data Grid inner scrollbar." }, "scrollEndThreshold": { - "description": "Set the area in px at the bottom of the grid viewport where onRowsScrollEnd is called." + "description": "Set the area in px at the bottom of the grid viewport where onRowsScrollEnd is called. If combined with unstable_lazyLoading, it defines the area where the next data request is triggered." }, "showCellVerticalBorder": { "description": "If true, vertical borders will be displayed between cells." @@ -596,6 +596,12 @@ "treeData": { "description": "If true, the rows will be gathered in a tree structure according to the getTreeDataPath prop." }, + "unstable_lazyLoading": { + "description": "Used together with unstable_dataSource to enable lazy loading. If enabled, the grid stops adding paginationModel to the data requests (getRows) and starts sending start and end values depending on the loading mode and the scroll position." + }, + "unstable_lazyLoadingRequestThrottleMs": { + "description": "If positive, the Data Grid will throttle data source requests on rendered rows interval change." + }, "unstable_listColumn": { "description": "Definition of the column rendered when the unstable_listView prop is enabled." }, diff --git a/packages/x-data-grid-generator/src/hooks/serverUtils.ts b/packages/x-data-grid-generator/src/hooks/serverUtils.ts index 80651b133af5..c0643d608e92 100644 --- a/packages/x-data-grid-generator/src/hooks/serverUtils.ts +++ b/packages/x-data-grid-generator/src/hooks/serverUtils.ts @@ -40,8 +40,8 @@ export interface QueryOptions { pageSize?: number; filterModel?: GridFilterModel; sortModel?: GridSortModel; - firstRowToRender?: number; - lastRowToRender?: number; + start?: number; + end?: number; } export interface ServerSideQueryOptions { @@ -50,8 +50,8 @@ export interface ServerSideQueryOptions { groupKeys?: string[]; filterModel?: GridFilterModel; sortModel?: GridSortModel; - firstRowToRender?: number; - lastRowToRender?: number; + start?: number; + end?: number; groupFields?: string[]; } @@ -277,7 +277,7 @@ export const loadServerRows = ( } const delay = randomInt(minDelay, maxDelay); - const { cursor, page = 0, pageSize } = queryOptions; + const { cursor, page = 0, pageSize, start, end } = queryOptions; let nextCursor; let firstRowIndex; @@ -289,22 +289,25 @@ export const loadServerRows = ( filteredRows = [...filteredRows].sort(rowComparator); const totalRowCount = filteredRows.length; - if (!pageSize) { + if (start !== undefined && end !== undefined) { + firstRowIndex = start; + lastRowIndex = end; + } else if (!pageSize) { firstRowIndex = 0; - lastRowIndex = filteredRows.length; + lastRowIndex = filteredRows.length - 1; } else if (useCursorPagination) { firstRowIndex = cursor ? filteredRows.findIndex(({ id }) => id === cursor) : 0; firstRowIndex = Math.max(firstRowIndex, 0); // if cursor not found return 0 - lastRowIndex = firstRowIndex + pageSize; + lastRowIndex = firstRowIndex + pageSize - 1; - nextCursor = lastRowIndex >= filteredRows.length ? undefined : filteredRows[lastRowIndex].id; + nextCursor = filteredRows[lastRowIndex + 1]?.id; } else { firstRowIndex = page * pageSize; - lastRowIndex = (page + 1) * pageSize; + lastRowIndex = (page + 1) * pageSize - 1; } const hasNextPage = lastRowIndex < filteredRows.length - 1; const response: FakeServerResponse = { - returnedRows: filteredRows.slice(firstRowIndex, lastRowIndex), + returnedRows: filteredRows.slice(firstRowIndex, lastRowIndex + 1), hasNextPage, nextCursor, totalRowCount, diff --git a/packages/x-data-grid-generator/src/hooks/useMockServer.ts b/packages/x-data-grid-generator/src/hooks/useMockServer.ts index e9c168f0ca08..9fb8aff79a8c 100644 --- a/packages/x-data-grid-generator/src/hooks/useMockServer.ts +++ b/packages/x-data-grid-generator/src/hooks/useMockServer.ts @@ -3,7 +3,6 @@ import { LRUCache } from 'lru-cache'; import { getGridDefaultColumnTypes, GridRowModel, - GridGetRowsParams, GridGetRowsResponse, GridColDef, GridInitialState, @@ -104,7 +103,7 @@ const getColumnsFromOptions = (options: ColumnsOptions): GridColDefGenerator[] | return columns; }; -function decodeParams(url: string): GridGetRowsParams { +function decodeParams(url: string) { const params = new URL(url).searchParams; const decodedParams = {} as any; const array = Array.from(params.entries()); @@ -117,7 +116,7 @@ function decodeParams(url: string): GridGetRowsParams { } } - return decodedParams as GridGetRowsParams; + return decodedParams; } const getInitialState = (columns: GridColDefGenerator[], groupingField?: string) => { diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index feaef4ba9547..45b734be6760 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -1004,7 +1004,7 @@ DataGridPremiumRaw.propTypes = { * Loading rows can be processed on the server or client-side. * Set it to 'client' if you would like enable infnite loading. * Set it to 'server' if you would like to enable lazy loading. - * * @default "client" + * @default "client" */ rowsLoadingMode: PropTypes.oneOf(['client', 'server']), /** @@ -1018,6 +1018,7 @@ DataGridPremiumRaw.propTypes = { scrollbarSize: PropTypes.number, /** * Set the area in `px` at the bottom of the grid viewport where onRowsScrollEnd is called. + * If combined with `unstable_lazyLoading`, it defines the area where the next data request is triggered. * @default 80 */ scrollEndThreshold: PropTypes.number, @@ -1097,6 +1098,18 @@ DataGridPremiumRaw.propTypes = { get: PropTypes.func.isRequired, set: PropTypes.func.isRequired, }), + /** + * Used together with `unstable_dataSource` to enable lazy loading. + * If enabled, the grid stops adding `paginationModel` to the data requests (`getRows`) + * and starts sending `start` and `end` values depending on the loading mode and the scroll position. + * @default false + */ + unstable_lazyLoading: PropTypes.bool, + /** + * If positive, the Data Grid will throttle data source requests on rendered rows interval change. + * @default 500 + */ + unstable_lazyLoadingRequestThrottleMs: PropTypes.number, /** * Definition of the column rendered when the `unstable_listView` prop is enabled. */ diff --git a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx index 46c76914dda7..5e3f20c6ea49 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx @@ -61,6 +61,7 @@ import { columnGroupsStateInitializer, useGridLazyLoader, useGridLazyLoaderPreProcessors, + useGridDataSourceLazyLoader, headerFilteringStateInitializer, useGridHeaderFiltering, virtualizationStateInitializer, @@ -180,6 +181,7 @@ export const useDataGridPremiumComponent = ( useGridScroll(apiRef, props); useGridInfiniteLoader(apiRef, props); useGridLazyLoader(apiRef, props); + useGridDataSourceLazyLoader(apiRef, props); useGridColumnMenu(apiRef); useGridCsvExport(apiRef, props); useGridPrintExport(apiRef, props); diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts index 0310fb7b338d..353b51936bd4 100644 --- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts +++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts @@ -19,6 +19,7 @@ import { GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, getRowGroupingCriteriaFromGroupingField, isGroupingColumn, + GridStrategyGroup, } from '@mui/x-data-grid-pro/internals'; import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps'; import { @@ -211,7 +212,7 @@ export const setStrategyAvailability = ( const strategy = dataSource ? RowGroupingStrategy.DataSource : RowGroupingStrategy.Default; - privateApiRef.current.setStrategyAvailability('rowTree', strategy, isAvailable); + privateApiRef.current.setStrategyAvailability(GridStrategyGroup.RowTree, strategy, isAvailable); }; export const getCellGroupingCriteria = ({ diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx index fe562ab900c3..b8b6779f1401 100644 --- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx +++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx @@ -10,6 +10,7 @@ import { GridPipeProcessor, GridRestoreStatePreProcessingContext, GridStateInitializer, + GridStrategyGroup, } from '@mui/x-data-grid-pro/internals'; import { GridPrivateApiPremium } from '../../../models/gridApiPremium'; import { @@ -275,7 +276,9 @@ export const useGridRowGrouping = ( // Refresh the row tree creation strategy processing // TODO: Add a clean way to re-run a strategy processing without publishing a private event - if (apiRef.current.getActiveStrategy('rowTree') === RowGroupingStrategy.Default) { + if ( + apiRef.current.getActiveStrategy(GridStrategyGroup.RowTree) === RowGroupingStrategy.Default + ) { apiRef.current.publishEvent('activeStrategyProcessorChange', 'rowTreeCreation'); } } diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 2f7225467c20..4a57c8bd8483 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -911,7 +911,7 @@ DataGridProRaw.propTypes = { * Loading rows can be processed on the server or client-side. * Set it to 'client' if you would like enable infnite loading. * Set it to 'server' if you would like to enable lazy loading. - * * @default "client" + * @default "client" */ rowsLoadingMode: PropTypes.oneOf(['client', 'server']), /** @@ -925,6 +925,7 @@ DataGridProRaw.propTypes = { scrollbarSize: PropTypes.number, /** * Set the area in `px` at the bottom of the grid viewport where onRowsScrollEnd is called. + * If combined with `unstable_lazyLoading`, it defines the area where the next data request is triggered. * @default 80 */ scrollEndThreshold: PropTypes.number, @@ -997,6 +998,18 @@ DataGridProRaw.propTypes = { get: PropTypes.func.isRequired, set: PropTypes.func.isRequired, }), + /** + * Used together with `unstable_dataSource` to enable lazy loading. + * If enabled, the grid stops adding `paginationModel` to the data requests (`getRows`) + * and starts sending `start` and `end` values depending on the loading mode and the scroll position. + * @default false + */ + unstable_lazyLoading: PropTypes.bool, + /** + * If positive, the Data Grid will throttle data source requests on rendered rows interval change. + * @default 500 + */ + unstable_lazyLoadingRequestThrottleMs: PropTypes.number, /** * Definition of the column rendered when the `unstable_listView` prop is enabled. */ diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx index 0240d83802fc..f997b0c8d7f3 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx @@ -86,6 +86,7 @@ import { useGridDataSource, dataSourceStateInitializer, } from '../hooks/features/dataSource/useGridDataSource'; +import { useGridDataSourceLazyLoader } from '../hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader'; export const useDataGridProComponent = ( inputApiRef: React.MutableRefObject | undefined, @@ -163,6 +164,7 @@ export const useDataGridProComponent = ( useGridScroll(apiRef, props); useGridInfiniteLoader(apiRef, props); useGridLazyLoader(apiRef, props); + useGridDataSourceLazyLoader(apiRef, props); useGridColumnMenu(apiRef); useGridCsvExport(apiRef, props); useGridPrintExport(apiRef, props); diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts index 295702230804..fffa481874e6 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts +++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts @@ -56,6 +56,8 @@ export const DATA_GRID_PRO_PROPS_DEFAULT_VALUES: DataGridProPropsWithDefaultValu scrollEndThreshold: 80, treeData: false, unstable_listView: false, + unstable_lazyLoading: false, + unstable_lazyLoadingRequestThrottleMs: 500, }; const defaultSlots = DATA_GRID_PRO_DEFAULT_SLOTS_COMPONENTS; diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts index 5645235abf01..0176587dbda9 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts @@ -1,6 +1,6 @@ import { GridGetRowsParams, GridGetRowsResponse } from '../../../models'; -type GridDataSourceCacheDefaultConfig = { +export type GridDataSourceCacheDefaultConfig = { /** * Time To Live for each cache entry in milliseconds. * After this time the cache entry will become stale and the next query will result in cache miss. @@ -11,11 +11,12 @@ type GridDataSourceCacheDefaultConfig = { function getKey(params: GridGetRowsParams) { return JSON.stringify([ - params.paginationModel, params.filterModel, params.sortModel, params.groupKeys, params.groupFields, + params.start, + params.end, ]); } @@ -41,10 +42,12 @@ export class GridDataSourceCacheDefault { if (!entry) { return undefined; } + if (Date.now() > entry.expiry) { delete this.cache[keyString]; return undefined; } + return entry.value; } diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/interfaces.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/interfaces.ts index 90bfc4ed39de..08eaf7c33f42 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/interfaces.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/interfaces.ts @@ -1,5 +1,5 @@ import { GridRowId } from '@mui/x-data-grid'; -import { GridDataSourceCache } from '../../../models'; +import { GridDataSourceCache, GridGetRowsParams } from '../../../models'; export interface GridDataSourceState { loading: Record; @@ -23,11 +23,13 @@ export interface GridDataSourceApiBase { */ setChildrenFetchError: (parentId: GridRowId, error: Error | null) => void; /** - * Fetches the rows from the server for a given `parentId`. - * If no `parentId` is provided, it fetches the root rows. - * @param {string} parentId The id of the group to be fetched. + * Fetches the rows from the server. + * If no `parentId` option is provided, it fetches the root rows. + * Any missing parameter from `params` will be filled from the state (sorting, filtering, etc.). + * @param {GridRowId} parentId The id of the parent node. + * @param {Partial} params Request parameters override. */ - fetchRows: (parentId?: GridRowId) => void; + fetchRows: (parentId?: GridRowId, params?: Partial) => void; /** * The data source cache object. */ diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts index 92b19b032d03..243af1a9e34c 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts @@ -2,20 +2,34 @@ import * as React from 'react'; import useLazyRef from '@mui/utils/useLazyRef'; import { useGridApiEventHandler, - gridRowsLoadingSelector, useGridApiMethod, GridDataSourceGroupNode, useGridSelector, - GridRowId, + gridPaginationModelSelector, + GRID_ROOT_GROUP_ID, + GridEventListener, } from '@mui/x-data-grid'; -import { gridRowGroupsToFetchSelector, GridStateInitializer } from '@mui/x-data-grid/internals'; +import { + GridGetRowsResponse, + gridRowGroupsToFetchSelector, + GridStateInitializer, + GridStrategyGroup, + GridStrategyProcessor, + useGridRegisterStrategyProcessor, +} from '@mui/x-data-grid/internals'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; import { gridGetRowsParamsSelector, gridDataSourceErrorsSelector } from './gridDataSourceSelector'; import { GridDataSourceApi, GridDataSourceApiBase, GridDataSourcePrivateApi } from './interfaces'; -import { runIfServerMode, NestedDataManager, RequestStatus } from './utils'; +import { + CacheChunkManager, + DataSourceRowsUpdateStrategy, + NestedDataManager, + RequestStatus, + runIf, +} from './utils'; import { GridDataSourceCache } from '../../../models'; -import { GridDataSourceCacheDefault } from './cache'; +import { GridDataSourceCacheDefault, GridDataSourceCacheDefaultConfig } from './cache'; const INITIAL_STATE = { loading: {}, @@ -28,11 +42,14 @@ const noopCache: GridDataSourceCache = { set: () => {}, }; -function getCache(cacheProp?: GridDataSourceCache | null) { +function getCache( + cacheProp?: GridDataSourceCache | null, + options: GridDataSourceCacheDefaultConfig = {}, +) { if (cacheProp === null) { return noopCache; } - return cacheProp ?? new GridDataSourceCacheDefault({}); + return cacheProp ?? new GridDataSourceCacheDefault(options); } export const dataSourceStateInitializer: GridStateInitializer = (state) => { @@ -52,28 +69,51 @@ export const useGridDataSource = ( | 'sortingMode' | 'filterMode' | 'paginationMode' + | 'pageSizeOptions' | 'treeData' + | 'unstable_lazyLoading' >, ) => { + const setStrategyAvailability = React.useCallback(() => { + apiRef.current.setStrategyAvailability( + GridStrategyGroup.DataSource, + DataSourceRowsUpdateStrategy.Default, + props.unstable_dataSource && !props.unstable_lazyLoading ? () => true : () => false, + ); + }, [apiRef, props.unstable_lazyLoading, props.unstable_dataSource]); + + const [defaultRowsUpdateStrategyActive, setDefaultRowsUpdateStrategyActive] = + React.useState(false); const nestedDataManager = useLazyRef( () => new NestedDataManager(apiRef), ).current; + const paginationModel = useGridSelector(apiRef, gridPaginationModelSelector); const groupsToAutoFetch = useGridSelector(apiRef, gridRowGroupsToFetchSelector); const scheduledGroups = React.useRef(0); + const lastRequestId = React.useRef(0); + const onError = props.unstable_onDataSourceError; + const cacheChunkManager = useLazyRef(() => { + const sortedPageSizeOptions = props.pageSizeOptions + .map((option) => (typeof option === 'number' ? option : option.value)) + .sort((a, b) => a - b); + const cacheChunkSize = Math.min(paginationModel.pageSize, sortedPageSizeOptions[0]); + + return new CacheChunkManager(cacheChunkSize); + }).current; const [cache, setCache] = React.useState(() => getCache(props.unstable_dataSourceCache), ); - const fetchRows = React.useCallback( - async (parentId?: GridRowId) => { + const fetchRows = React.useCallback( + async (parentId, params) => { const getRows = props.unstable_dataSource?.getRows; if (!getRows) { return; } - if (parentId) { + if (parentId && parentId !== GRID_ROOT_GROUP_ID) { nestedDataManager.queue([parentId]); return; } @@ -88,39 +128,65 @@ export const useGridDataSource = ( const fetchParams = { ...gridGetRowsParamsSelector(apiRef), ...apiRef.current.unstable_applyPipeProcessors('getRowsParams', {}), + ...params, }; - const cachedData = apiRef.current.unstable_dataSource.cache.get(fetchParams); + const cacheKeys = cacheChunkManager.getCacheKeys(fetchParams); + const responses = cacheKeys.map((cacheKey) => cache.get(cacheKey)); - if (cachedData !== undefined) { - const rows = cachedData.rows; - apiRef.current.setRows(rows); - if (cachedData.rowCount !== undefined) { - apiRef.current.setRowCount(cachedData.rowCount); - } + if (responses.every((response) => response !== undefined)) { + apiRef.current.applyStrategyProcessor('dataSourceRowsUpdate', { + response: CacheChunkManager.mergeResponses(responses as GridGetRowsResponse[]), + fetchParams, + }); return; } - const isLoading = gridRowsLoadingSelector(apiRef); - if (!isLoading) { + // Manage loading state only for the default strategy + if (defaultRowsUpdateStrategyActive || apiRef.current.getRowsCount() === 0) { apiRef.current.setLoading(true); } + const requestId = lastRequestId.current + 1; + lastRequestId.current = requestId; + try { const getRowsResponse = await getRows(fetchParams); - apiRef.current.unstable_dataSource.cache.set(fetchParams, getRowsResponse); - if (getRowsResponse.rowCount !== undefined) { - apiRef.current.setRowCount(getRowsResponse.rowCount); + + const cacheResponses = cacheChunkManager.splitResponse(fetchParams, getRowsResponse); + cacheResponses.forEach((response, key) => { + cache.set(key, response); + }); + + if (lastRequestId.current === requestId) { + apiRef.current.applyStrategyProcessor('dataSourceRowsUpdate', { + response: getRowsResponse, + fetchParams, + }); } - apiRef.current.setRows(getRowsResponse.rows); - apiRef.current.setLoading(false); } catch (error) { - apiRef.current.setRows([]); - apiRef.current.setLoading(false); - onError?.(error as Error, fetchParams); + if (lastRequestId.current === requestId) { + apiRef.current.applyStrategyProcessor('dataSourceRowsUpdate', { + error: error as Error, + fetchParams, + }); + onError?.(error as Error, fetchParams); + } + } finally { + if (defaultRowsUpdateStrategyActive && lastRequestId.current === requestId) { + apiRef.current.setLoading(false); + } } }, - [nestedDataManager, apiRef, props.unstable_dataSource?.getRows, onError], + [ + nestedDataManager, + cacheChunkManager, + cache, + apiRef, + defaultRowsUpdateStrategyActive, + props.unstable_dataSource?.getRows, + onError, + ], ); const fetchRowChildren = React.useCallback( @@ -148,15 +214,17 @@ export const useGridDataSource = ( groupKeys: rowNode.path, }; - const cachedData = apiRef.current.unstable_dataSource.cache.get(fetchParams); + const cacheKeys = cacheChunkManager.getCacheKeys(fetchParams); + const responses = cacheKeys.map((cacheKey) => cache.get(cacheKey)); + const cachedData = responses.some((response) => response === undefined) + ? undefined + : CacheChunkManager.mergeResponses(responses as GridGetRowsResponse[]); if (cachedData !== undefined) { const rows = cachedData.rows; nestedDataManager.setRequestSettled(id); apiRef.current.updateServerRows(rows, rowNode.path); - if (cachedData.rowCount) { - apiRef.current.setRowCount(cachedData.rowCount); - } + apiRef.current.setRowCount(cachedData.rowCount === undefined ? -1 : cachedData.rowCount); apiRef.current.setRowChildrenExpansion(id, true); apiRef.current.unstable_dataSource.setChildrenLoading(id, false); return; @@ -179,10 +247,15 @@ export const useGridDataSource = ( return; } nestedDataManager.setRequestSettled(id); - apiRef.current.unstable_dataSource.cache.set(fetchParams, getRowsResponse); - if (getRowsResponse.rowCount) { - apiRef.current.setRowCount(getRowsResponse.rowCount); - } + + const cacheResponses = cacheChunkManager.splitResponse(fetchParams, getRowsResponse); + cacheResponses.forEach((response, key) => { + cache.set(key, response); + }); + + apiRef.current.setRowCount( + getRowsResponse.rowCount === undefined ? -1 : getRowsResponse.rowCount, + ); apiRef.current.updateServerRows(getRowsResponse.rows, rowNode.path); apiRef.current.setRowChildrenExpansion(id, true); } catch (error) { @@ -194,7 +267,15 @@ export const useGridDataSource = ( nestedDataManager.setRequestSettled(id); } }, - [nestedDataManager, onError, apiRef, props.treeData, props.unstable_dataSource?.getRows], + [ + nestedDataManager, + cacheChunkManager, + cache, + onError, + apiRef, + props.treeData, + props.unstable_dataSource?.getRows, + ], ); const setChildrenLoading = React.useCallback( @@ -242,6 +323,31 @@ export const useGridDataSource = ( [apiRef], ); + const handleStrategyActivityChange = React.useCallback< + GridEventListener<'strategyAvailabilityChange'> + >(() => { + setDefaultRowsUpdateStrategyActive( + apiRef.current.getActiveStrategy(GridStrategyGroup.DataSource) === + DataSourceRowsUpdateStrategy.Default, + ); + }, [apiRef]); + + const handleDataUpdate = React.useCallback>( + (params) => { + if ('error' in params) { + apiRef.current.setRows([]); + return; + } + + const { response } = params; + if (response.rowCount !== undefined) { + apiRef.current.setRowCount(response.rowCount); + } + apiRef.current.setRows(response.rows); + }, + [apiRef], + ); + const resetDataSourceState = React.useCallback(() => { apiRef.current.setState((state) => { return { @@ -268,12 +374,28 @@ export const useGridDataSource = ( useGridApiMethod(apiRef, dataSourceApi, 'public'); useGridApiMethod(apiRef, dataSourcePrivateApi, 'private'); - useGridApiEventHandler(apiRef, 'sortModelChange', runIfServerMode(props.sortingMode, fetchRows)); - useGridApiEventHandler(apiRef, 'filterModelChange', runIfServerMode(props.filterMode, fetchRows)); + useGridRegisterStrategyProcessor( + apiRef, + DataSourceRowsUpdateStrategy.Default, + 'dataSourceRowsUpdate', + handleDataUpdate, + ); + + useGridApiEventHandler(apiRef, 'strategyAvailabilityChange', handleStrategyActivityChange); + useGridApiEventHandler( + apiRef, + 'sortModelChange', + runIf(defaultRowsUpdateStrategyActive, () => fetchRows()), + ); + useGridApiEventHandler( + apiRef, + 'filterModelChange', + runIf(defaultRowsUpdateStrategyActive, () => fetchRows()), + ); useGridApiEventHandler( apiRef, 'paginationModelChange', - runIfServerMode(props.paginationMode, fetchRows), + runIf(defaultRowsUpdateStrategyActive, () => fetchRows()), ); const isFirstRender = React.useRef(true); @@ -286,6 +408,10 @@ export const useGridDataSource = ( setCache((prevCache) => (prevCache !== newCache ? newCache : prevCache)); }, [props.unstable_dataSourceCache]); + React.useEffect(() => { + setStrategyAvailability(); + }, [setStrategyAvailability]); + React.useEffect(() => { if (props.unstable_dataSource) { apiRef.current.unstable_dataSource.cache.clear(); diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts index dafc6d9783f2..ce2d8de13339 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts @@ -1,11 +1,11 @@ import { GridRowId } from '@mui/x-data-grid'; -import { GridPrivateApiPro } from '../../../models/gridApiPro'; +import { GridPrivateApiPro, GridGetRowsParams, GridGetRowsResponse } from '../../../models'; const MAX_CONCURRENT_REQUESTS = Infinity; -export const runIfServerMode = (modeProp: 'server' | 'client', fn: Function) => () => { - if (modeProp === 'server') { - fn(); +export const runIf = (condition: boolean, fn: Function) => (params: unknown) => { + if (condition) { + fn(params); } }; @@ -16,6 +16,11 @@ export enum RequestStatus { UNKNOWN, } +export enum DataSourceRowsUpdateStrategy { + Default = 'set-new-rows', + LazyLoading = 'replace-row-range', +} + /** * Fetches row children from the server with option to limit the number of concurrent requests * Determines the status of a request based on the enum `RequestStatus` @@ -112,3 +117,83 @@ export class NestedDataManager { public getActiveRequestsCount = () => this.pendingRequests.size + this.queuedRequests.size; } + +/** + * Provides better cache hit rate by: + * 1. Splitting the data into smaller chunks to be stored in the cache (cache `set`) + * 2. Merging multiple cache entries into a single response to get the required chunk (cache `get`) + */ +export class CacheChunkManager { + private chunkSize: number; + + /** + * @param chunkSize The number of rows to store in each cache entry. + * If not set, the whole array will be stored in a single cache entry. + * Setting this value to smallest page size will result in better cache hit rate. + * Has no effect if cursor pagination is used. + */ + constructor(chunkSize: number) { + this.chunkSize = chunkSize; + } + + public getCacheKeys = (key: GridGetRowsParams) => { + if (this.chunkSize < 1 || typeof key.start !== 'number') { + return [key]; + } + + // split the range into chunks + const chunkedKeys: GridGetRowsParams[] = []; + for (let i = key.start; i < key.end; i += this.chunkSize) { + const end = Math.min(i + this.chunkSize - 1, key.end); + chunkedKeys.push({ ...key, start: i, end }); + } + + return chunkedKeys; + }; + + public splitResponse = (key: GridGetRowsParams, response: GridGetRowsResponse) => { + const cacheKeys = this.getCacheKeys(key); + const responses = new Map(); + cacheKeys.forEach((chunkKey) => { + const isLastChunk = chunkKey.end === key.end; + const responseSlice: GridGetRowsResponse = { + ...response, + pageInfo: { + ...response.pageInfo, + // If the original response had page info, update that information for all but last chunk and keep the original value for the last chunk + hasNextPage: + response.pageInfo?.hasNextPage !== undefined && !isLastChunk + ? true + : response.pageInfo?.hasNextPage, + nextCursor: + response.pageInfo?.nextCursor !== undefined && !isLastChunk + ? response.rows[chunkKey.end + 1].id + : response.pageInfo?.nextCursor, + }, + rows: + typeof chunkKey.start !== 'number' || typeof key.start !== 'number' + ? response.rows + : response.rows.slice(chunkKey.start - key.start, chunkKey.end - key.start + 1), + }; + + responses.set(chunkKey, responseSlice); + }); + + return responses; + }; + + static mergeResponses = (responses: GridGetRowsResponse[]): GridGetRowsResponse => { + if (responses.length === 1) { + return responses[0]; + } + + return responses.reduce( + (acc, response) => ({ + rows: [...acc.rows, ...response.rows], + rowCount: response.rowCount, + pageInfo: response.pageInfo, + }), + { rows: [], rowCount: 0, pageInfo: {} }, + ); + }; +} diff --git a/packages/x-data-grid-pro/src/hooks/features/index.ts b/packages/x-data-grid-pro/src/hooks/features/index.ts index dd9209be6a53..456da75b157c 100644 --- a/packages/x-data-grid-pro/src/hooks/features/index.ts +++ b/packages/x-data-grid-pro/src/hooks/features/index.ts @@ -6,4 +6,4 @@ export * from './treeData'; export * from './detailPanel'; export * from './rowPinning'; export * from './dataSource/interfaces'; -export * from './dataSource/cache'; +export { GridDataSourceCacheDefault } from './dataSource/cache'; diff --git a/packages/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts b/packages/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts index 0ced8d346ae7..2041a6f6b485 100644 --- a/packages/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts +++ b/packages/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts @@ -7,56 +7,12 @@ import { gridRenderContextSelector, useGridApiOptionHandler, GridEventListener, - GridRowEntry, } from '@mui/x-data-grid'; import { getVisibleRows } from '@mui/x-data-grid/internals'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; import { GridFetchRowsParams } from '../../../models/gridFetchRowsParams'; - -function findSkeletonRowsSection({ - apiRef, - visibleRows, - range, -}: { - apiRef: React.MutableRefObject; - visibleRows: GridRowEntry[]; - range: { firstRowIndex: number; lastRowIndex: number }; -}) { - let { firstRowIndex, lastRowIndex } = range; - const visibleRowsSection = visibleRows.slice(range.firstRowIndex, range.lastRowIndex); - let startIndex = 0; - let endIndex = visibleRowsSection.length - 1; - let isSkeletonSectionFound = false; - - while (!isSkeletonSectionFound && firstRowIndex < lastRowIndex) { - const isStartingWithASkeletonRow = - apiRef.current.getRowNode(visibleRowsSection[startIndex].id)?.type === 'skeletonRow'; - const isEndingWithASkeletonRow = - apiRef.current.getRowNode(visibleRowsSection[endIndex].id)?.type === 'skeletonRow'; - - if (isStartingWithASkeletonRow && isEndingWithASkeletonRow) { - isSkeletonSectionFound = true; - } - - if (!isStartingWithASkeletonRow) { - startIndex += 1; - firstRowIndex += 1; - } - - if (!isEndingWithASkeletonRow) { - endIndex -= 1; - lastRowIndex -= 1; - } - } - - return isSkeletonSectionFound - ? { - firstRowIndex, - lastRowIndex, - } - : undefined; -} +import { findSkeletonRowsSection } from './utils'; /** * @requires useGridRows (state) diff --git a/packages/x-data-grid-pro/src/hooks/features/lazyLoader/utils.ts b/packages/x-data-grid-pro/src/hooks/features/lazyLoader/utils.ts new file mode 100644 index 000000000000..584d3696d17a --- /dev/null +++ b/packages/x-data-grid-pro/src/hooks/features/lazyLoader/utils.ts @@ -0,0 +1,46 @@ +import { GridRowEntry } from '@mui/x-data-grid'; +import { GridPrivateApiPro } from '../../../models/gridApiPro'; + +export const findSkeletonRowsSection = ({ + apiRef, + visibleRows, + range, +}: { + apiRef: React.MutableRefObject; + visibleRows: GridRowEntry[]; + range: { firstRowIndex: number; lastRowIndex: number }; +}) => { + let { firstRowIndex, lastRowIndex } = range; + const visibleRowsSection = visibleRows.slice(range.firstRowIndex, range.lastRowIndex); + let startIndex = 0; + let endIndex = visibleRowsSection.length - 1; + let isSkeletonSectionFound = false; + + while (!isSkeletonSectionFound && firstRowIndex < lastRowIndex) { + const isStartingWithASkeletonRow = + apiRef.current.getRowNode(visibleRowsSection[startIndex].id)?.type === 'skeletonRow'; + const isEndingWithASkeletonRow = + apiRef.current.getRowNode(visibleRowsSection[endIndex].id)?.type === 'skeletonRow'; + + if (isStartingWithASkeletonRow && isEndingWithASkeletonRow) { + isSkeletonSectionFound = true; + } + + if (!isStartingWithASkeletonRow) { + startIndex += 1; + firstRowIndex += 1; + } + + if (!isEndingWithASkeletonRow) { + endIndex -= 1; + lastRowIndex -= 1; + } + } + + return isSkeletonSectionFound + ? { + firstRowIndex, + lastRowIndex, + } + : undefined; +}; diff --git a/packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts b/packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts new file mode 100644 index 000000000000..2b940e816308 --- /dev/null +++ b/packages/x-data-grid-pro/src/hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader.ts @@ -0,0 +1,512 @@ +import * as React from 'react'; +import { throttle } from '@mui/x-internals/throttle'; +import { + useGridApiEventHandler, + useGridSelector, + gridSortModelSelector, + gridFilterModelSelector, + GridEventListener, + GRID_ROOT_GROUP_ID, + GridGroupNode, + GridSkeletonRowNode, + gridPaginationModelSelector, + gridDimensionsSelector, + gridFilteredSortedRowIdsSelector, +} from '@mui/x-data-grid'; +import { + getVisibleRows, + GridGetRowsParams, + gridRenderContextSelector, + GridStrategyGroup, + GridStrategyProcessor, + useGridRegisterStrategyProcessor, +} from '@mui/x-data-grid/internals'; +import { GridPrivateApiPro } from '../../../models/gridApiPro'; +import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; +import { findSkeletonRowsSection } from '../lazyLoader/utils'; +import { GRID_SKELETON_ROW_ROOT_ID } from '../lazyLoader/useGridLazyLoaderPreProcessors'; +import { DataSourceRowsUpdateStrategy, runIf } from '../dataSource/utils'; + +enum LoadingTrigger { + VIEWPORT, + SCROLL_END, +} + +const INTERVAL_CACHE_INITIAL_STATE = { + firstRowToRender: 0, + lastRowToRender: 0, +}; + +const getSkeletonRowId = (index: number) => `${GRID_SKELETON_ROW_ROOT_ID}-${index}`; + +/** + * @requires useGridRows (state) + * @requires useGridPagination (state) + * @requires useGridDimensions (method) - can be after + * @requires useGridScroll (method + */ +export const useGridDataSourceLazyLoader = ( + privateApiRef: React.MutableRefObject, + props: Pick< + DataGridProProcessedProps, + | 'pagination' + | 'paginationMode' + | 'unstable_dataSource' + | 'unstable_lazyLoading' + | 'unstable_lazyLoadingRequestThrottleMs' + | 'scrollEndThreshold' + >, +): void => { + const setStrategyAvailability = React.useCallback(() => { + privateApiRef.current.setStrategyAvailability( + GridStrategyGroup.DataSource, + DataSourceRowsUpdateStrategy.LazyLoading, + props.unstable_dataSource && props.unstable_lazyLoading ? () => true : () => false, + ); + }, [privateApiRef, props.unstable_lazyLoading, props.unstable_dataSource]); + + const [lazyLoadingRowsUpdateStrategyActive, setLazyLoadingRowsUpdateStrategyActive] = + React.useState(false); + const sortModel = useGridSelector(privateApiRef, gridSortModelSelector); + const filterModel = useGridSelector(privateApiRef, gridFilterModelSelector); + const paginationModel = useGridSelector(privateApiRef, gridPaginationModelSelector); + const filteredSortedRowIds = useGridSelector(privateApiRef, gridFilteredSortedRowIdsSelector); + const dimensions = useGridSelector(privateApiRef, gridDimensionsSelector); + const renderContext = useGridSelector(privateApiRef, gridRenderContextSelector); + const renderedRowsIntervalCache = React.useRef(INTERVAL_CACHE_INITIAL_STATE); + const previousLastRowIndex = React.useRef(0); + const loadingTrigger = React.useRef(null); + const rowsStale = React.useRef(false); + + // 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: GridGetRowsParams) => { + if (typeof params.start !== 'number') { + return params; + } + + return { + ...params, + start: params.start - (params.start % paginationModel.pageSize), + end: params.end + paginationModel.pageSize - (params.end % paginationModel.pageSize) - 1, + }; + }, + [paginationModel], + ); + + const resetGrid = React.useCallback(() => { + privateApiRef.current.setLoading(true); + privateApiRef.current.unstable_dataSource.cache.clear(); + rowsStale.current = true; + previousLastRowIndex.current = 0; + const getRowsParams: GridGetRowsParams = { + start: 0, + end: paginationModel.pageSize - 1, + sortModel, + filterModel, + }; + + privateApiRef.current.unstable_dataSource.fetchRows(GRID_ROOT_GROUP_ID, getRowsParams); + }, [privateApiRef, sortModel, filterModel, paginationModel.pageSize]); + + const ensureValidRowCount = React.useCallback( + (previousLoadingTrigger: LoadingTrigger, newLoadingTrigger: LoadingTrigger) => { + // 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] as GridGroupNode; + 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] as GridGroupNode; + const rootGroupChildren = [...rootGroup.children]; + + const pageRowCount = privateApiRef.current.state.pagination.rowCount; + const rootChildrenCount = rootGroupChildren.length; + + /** + * Do nothing if + * - rowCount is unknown + * - children count is 0 + * - children count is equal to rowCount + */ + if ( + pageRowCount === -1 || + pageRowCount === undefined || + rootChildrenCount === 0 || + rootChildrenCount === pageRowCount + ) { + return; + } + + // 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: GridSkeletonRowNode = { + type: 'skeletonRow', + id: skeletonId, + parent: GRID_ROOT_GROUP_ID, + depth: 0, + }; + + tree[skeletonId] = skeletonRowNode; + } + + tree[GRID_ROOT_GROUP_ID] = { ...rootGroup, children: rootGroupChildren }; + + privateApiRef.current.setState( + (state) => ({ + ...state, + rows: { + ...state.rows, + tree, + }, + }), + 'addSkeletonRows', + ); + }, [privateApiRef]); + + const rebuildSkeletonRows = React.useCallback(() => { + // replace all data rows with skeleton rows. + const tree = privateApiRef.current.state.rows.tree; + const rootGroup = tree[GRID_ROOT_GROUP_ID] as GridGroupNode; + const rootGroupChildren = [...rootGroup.children]; + + for (let i = 0; i < rootGroupChildren.length; i += 1) { + if (tree[rootGroupChildren[i]]?.type === 'skeletonRow') { + continue; + } + + const skeletonId = getSkeletonRowId(i); + rootGroupChildren[i] = skeletonId; + + const skeletonRowNode: GridSkeletonRowNode = { + type: 'skeletonRow', + id: skeletonId, + parent: GRID_ROOT_GROUP_ID, + depth: 0, + }; + + tree[rootGroupChildren[i]] = skeletonRowNode; + } + + tree[GRID_ROOT_GROUP_ID] = { ...rootGroup, children: rootGroupChildren }; + + privateApiRef.current.setState( + (state) => ({ + ...state, + rows: { + ...state.rows, + tree, + }, + }), + 'addSkeletonRows', + ); + }, [privateApiRef]); + + const updateLoadingTrigger = React.useCallback( + (rowCount: number) => { + const newLoadingTrigger = + rowCount === -1 ? LoadingTrigger.SCROLL_END : LoadingTrigger.VIEWPORT; + + if (loadingTrigger.current !== newLoadingTrigger) { + loadingTrigger.current = newLoadingTrigger; + } + + if (loadingTrigger.current !== null) { + ensureValidRowCount(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; + 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 { + // having stale rows while not having a request for the first page means that the scroll position should be maintained + // convert all existing data to skeleton rows to avoid duplicate keys + if (rowsStale.current) { + rebuildSkeletonRows(); + } + + const startingIndex = + typeof fetchParams.start === 'string' + ? Math.max(filteredSortedRowIds.indexOf(fetchParams.start), 0) + : fetchParams.start; + + 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.requestPipeProcessorsApplication('hydrateRows'); + }, + [ + privateApiRef, + filteredSortedRowIds, + updateLoadingTrigger, + rebuildSkeletonRows, + addSkeletonRows, + ], + ); + + const handleRowCountChange = React.useCallback(() => { + if (loadingTrigger.current === null) { + return; + } + + updateLoadingTrigger(privateApiRef.current.state.pagination.rowCount); + addSkeletonRows(); + privateApiRef.current.requestPipeProcessorsApplication('hydrateRows'); + }, [privateApiRef, updateLoadingTrigger, addSkeletonRows]); + + const handleScrolling: GridEventListener<'scrollPositionChange'> = React.useCallback( + (newScrollPosition) => { + if ( + loadingTrigger.current !== LoadingTrigger.SCROLL_END || + previousLastRowIndex.current >= renderContext.lastRowIndex + ) { + return; + } + + const position = newScrollPosition.top + dimensions.viewportInnerSize.height; + const target = dimensions.contentSize.height - props.scrollEndThreshold; + + if (position >= target) { + previousLastRowIndex.current = renderContext.lastRowIndex; + + const getRowsParams: GridGetRowsParams = { + start: renderContext.lastRowIndex, + end: renderContext.lastRowIndex + paginationModel.pageSize - 1, + sortModel, + filterModel, + }; + + privateApiRef.current.setLoading(true); + privateApiRef.current.unstable_dataSource.fetchRows( + GRID_ROOT_GROUP_ID, + adjustRowParams(getRowsParams), + ); + } + }, + [ + privateApiRef, + props.scrollEndThreshold, + sortModel, + filterModel, + dimensions, + paginationModel.pageSize, + renderContext.lastRowIndex, + adjustRowParams, + ], + ); + + const handleRenderedRowsIntervalChange = React.useCallback< + GridEventListener<'renderedRowsIntervalChange'> + >( + (params) => { + if (loadingTrigger.current !== LoadingTrigger.VIEWPORT) { + return; + } + + const getRowsParams: GridGetRowsParams = { + start: params.firstRowIndex, + end: params.lastRowIndex, + 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, { + pagination: props.pagination, + paginationMode: props.paginationMode, + }); + + const skeletonRowsSection = findSkeletonRowsSection({ + apiRef: privateApiRef, + visibleRows: currentVisibleRows.rows, + range: { + firstRowIndex: params.firstRowIndex, + lastRowIndex: params.lastRowIndex, + }, + }); + + if (!skeletonRowsSection) { + return; + } + + getRowsParams.start = skeletonRowsSection.firstRowIndex; + getRowsParams.end = skeletonRowsSection.lastRowIndex; + + privateApiRef.current.unstable_dataSource.fetchRows( + GRID_ROOT_GROUP_ID, + adjustRowParams(getRowsParams), + ); + }, + [ + privateApiRef, + props.pagination, + props.paginationMode, + sortModel, + filterModel, + adjustRowParams, + ], + ); + + const throttledHandleRenderedRowsIntervalChange = React.useMemo( + () => throttle(handleRenderedRowsIntervalChange, props.unstable_lazyLoadingRequestThrottleMs), + [props.unstable_lazyLoadingRequestThrottleMs, handleRenderedRowsIntervalChange], + ); + + const handleGridSortModelChange = React.useCallback>( + (newSortModel) => { + rowsStale.current = true; + previousLastRowIndex.current = 0; + const rangeParams = + loadingTrigger.current === LoadingTrigger.VIEWPORT + ? { + start: renderContext.firstRowIndex, + end: renderContext.lastRowIndex, + } + : { + start: 0, + end: paginationModel.pageSize - 1, + }; + + const getRowsParams: GridGetRowsParams = { + ...rangeParams, + sortModel: newSortModel, + filterModel, + }; + + privateApiRef.current.setLoading(true); + privateApiRef.current.unstable_dataSource.fetchRows( + GRID_ROOT_GROUP_ID, + adjustRowParams(getRowsParams), + ); + }, + [privateApiRef, filterModel, paginationModel.pageSize, renderContext, adjustRowParams], + ); + + const handleGridFilterModelChange = React.useCallback>( + (newFilterModel) => { + rowsStale.current = true; + previousLastRowIndex.current = 0; + const getRowsParams: GridGetRowsParams = { + start: 0, + end: paginationModel.pageSize - 1, + sortModel, + filterModel: newFilterModel, + }; + + privateApiRef.current.setLoading(true); + privateApiRef.current.unstable_dataSource.fetchRows(GRID_ROOT_GROUP_ID, getRowsParams); + }, + [privateApiRef, sortModel, paginationModel.pageSize], + ); + + const handleStrategyActivityChange = React.useCallback< + GridEventListener<'strategyAvailabilityChange'> + >(() => { + setLazyLoadingRowsUpdateStrategyActive( + privateApiRef.current.getActiveStrategy(GridStrategyGroup.DataSource) === + DataSourceRowsUpdateStrategy.LazyLoading, + ); + }, [privateApiRef]); + + useGridRegisterStrategyProcessor( + privateApiRef, + DataSourceRowsUpdateStrategy.LazyLoading, + 'dataSourceRowsUpdate', + handleDataUpdate, + ); + + useGridApiEventHandler(privateApiRef, 'strategyAvailabilityChange', handleStrategyActivityChange); + + useGridApiEventHandler( + privateApiRef, + 'rowCountChange', + runIf(lazyLoadingRowsUpdateStrategyActive, handleRowCountChange), + ); + useGridApiEventHandler( + privateApiRef, + 'scrollPositionChange', + runIf(lazyLoadingRowsUpdateStrategyActive, handleScrolling), + ); + useGridApiEventHandler( + privateApiRef, + 'renderedRowsIntervalChange', + runIf(lazyLoadingRowsUpdateStrategyActive, throttledHandleRenderedRowsIntervalChange), + ); + useGridApiEventHandler( + privateApiRef, + 'sortModelChange', + runIf(lazyLoadingRowsUpdateStrategyActive, handleGridSortModelChange), + ); + useGridApiEventHandler( + privateApiRef, + 'filterModelChange', + runIf(lazyLoadingRowsUpdateStrategyActive, handleGridFilterModelChange), + ); + + React.useEffect(() => { + setStrategyAvailability(); + }, [setStrategyAvailability]); +}; diff --git a/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx b/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx index 0c6b841f939d..f2bc190bade4 100644 --- a/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx +++ b/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx @@ -11,6 +11,7 @@ import { import { GridPipeProcessor, GridRowsPartialUpdates, + GridStrategyGroup, GridStrategyProcessor, useGridRegisterPipeProcessor, useGridRegisterStrategyProcessor, @@ -51,7 +52,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( ) => { const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability( - 'rowTree', + GridStrategyGroup.RowTree, TreeDataStrategy.DataSource, props.treeData && props.unstable_dataSource ? () => true : () => false, ); diff --git a/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx b/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx index 6a5753c9eb60..82ae012ad851 100644 --- a/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx +++ b/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx @@ -10,6 +10,7 @@ import { } from '@mui/x-data-grid'; import { GridPipeProcessor, + GridStrategyGroup, GridStrategyProcessor, useGridRegisterPipeProcessor, useGridRegisterStrategyProcessor, @@ -51,7 +52,7 @@ export const useGridTreeDataPreProcessors = ( ) => { const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability( - 'rowTree', + GridStrategyGroup.RowTree, TreeDataStrategy.Default, props.treeData && !props.unstable_dataSource ? () => true : () => false, ); diff --git a/packages/x-data-grid-pro/src/internals/index.ts b/packages/x-data-grid-pro/src/internals/index.ts index ed619bb0cc9e..f821f3417011 100644 --- a/packages/x-data-grid-pro/src/internals/index.ts +++ b/packages/x-data-grid-pro/src/internals/index.ts @@ -43,6 +43,7 @@ export { } from '../hooks/features/rowPinning/useGridRowPinningPreProcessors'; export { useGridLazyLoader } from '../hooks/features/lazyLoader/useGridLazyLoader'; export { useGridLazyLoaderPreProcessors } from '../hooks/features/lazyLoader/useGridLazyLoaderPreProcessors'; +export { useGridDataSourceLazyLoader } from '../hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader'; export { useGridDataSource, dataSourceStateInitializer, diff --git a/packages/x-data-grid-pro/src/internals/propValidation.ts b/packages/x-data-grid-pro/src/internals/propValidation.ts index 13f138529d47..06367108d22e 100644 --- a/packages/x-data-grid-pro/src/internals/propValidation.ts +++ b/packages/x-data-grid-pro/src/internals/propValidation.ts @@ -31,4 +31,10 @@ export const propValidatorsDataGridPro: PropValidator isNumber(props.rowCount) && 'MUI X: Usage of the `rowCount` prop with client side pagination (`paginationMode="client"`) has no effect. `rowCount` is only meant to be used with `paginationMode="server"`.') || undefined, + (props) => + (props.signature !== GridSignature.DataGrid && + (props.rowsLoadingMode === 'server' || props.onRowsScrollEnd) && + props.unstable_lazyLoading && + 'MUI X: Usage of the client side lazy loading (`rowsLoadingMode="server"` or `onRowsScrollEnd=...`) cannot be used together with server side lazy loading `unstable_lazyLoading="true"`.') || + undefined, ]; diff --git a/packages/x-data-grid-pro/src/models/dataGridProProps.ts b/packages/x-data-grid-pro/src/models/dataGridProProps.ts index d2e2c490600d..af091b4e9340 100644 --- a/packages/x-data-grid-pro/src/models/dataGridProProps.ts +++ b/packages/x-data-grid-pro/src/models/dataGridProProps.ts @@ -77,6 +77,7 @@ export interface DataGridProPropsWithDefaultValue - Data source lazy loader', () => { + const { render } = createRenderer(); + const defaultTransformGetRowsResponse = (response: GridGetRowsResponse) => response; + + let transformGetRowsResponse: (response: GridGetRowsResponse) => GridGetRowsResponse; + let apiRef: React.MutableRefObject; + let fetchRowsSpy: SinonSpy; + let mockServer: ReturnType; + + function TestDataSourceLazyLoader(props: Partial) { + apiRef = useGridApiRef(); + mockServer = useMockServer( + { rowLength: 100, maxColumns: 1 }, + { useCursorPagination: false, minDelay: 0, maxDelay: 0, verbose: false }, + ); + fetchRowsSpy = spy(mockServer, 'fetchRows'); + const { fetchRows } = mockServer; + + const dataSource: GridDataSource = React.useMemo( + () => ({ + getRows: async (params: GridGetRowsParams) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + const response = transformGetRowsResponse(getRowsResponse); + return { + rows: response.rows, + rowCount: response.rowCount, + }; + }, + }), + [fetchRows], + ); + + const baselineProps = { + unstable_dataSource: dataSource, + columns: mockServer.columns, + unstable_lazyLoading: true, + paginationModel: { page: 0, pageSize: 10 }, + disableVirtualization: true, + }; + + return ( +
+ +
+ ); + } + + beforeEach(function beforeTest() { + if (isJSDOM) { + this.skip(); // Needs layout + } + + transformGetRowsResponse = defaultTransformGetRowsResponse; + }); + + it('should load the first page initially', async () => { + render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + }); + + describe('Viewport loading', () => { + it('should render skeleton rows if rowCount is bigger than the number of rows', async () => { + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + // The 11th row should be a skeleton + expect(getRow(10).dataset.id).to.equal('auto-generated-skeleton-row-root-10'); + }); + + it('should make a new data source request once the skeleton rows are in the render context', async () => { + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + // reset the spy call count + fetchRowsSpy.resetHistory(); + + apiRef.current.scrollToIndexes({ rowIndex: 10 }); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + }); + + it('should keep the scroll position when sorting is applied', async () => { + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + const initialSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + expect(initialSearchParams.get('end')).to.equal('9'); + + apiRef.current.scrollToIndexes({ rowIndex: 10 }); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + + const beforeSortSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + expect(beforeSortSearchParams.get('end')).to.not.equal('9'); + + apiRef.current.sortColumn(mockServer.columns[0].field, 'asc'); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(3); + }); + + const afterSortSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + expect(afterSortSearchParams.get('end')).to.equal(beforeSortSearchParams.get('end')); + }); + + it('should reset the scroll position when filter is applied', async () => { + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + apiRef.current.scrollToIndexes({ rowIndex: 10 }); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + + const beforeFilteringSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + // first row is not the first page anymore + expect(beforeFilteringSearchParams.get('start')).to.not.equal('0'); + + apiRef.current.setFilterModel({ + items: [ + { + field: mockServer.columns[0].field, + value: '0', + operator: 'contains', + }, + ], + }); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(3); + }); + + const afterFilteringSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + // first row is the start of the first page + expect(afterFilteringSearchParams.get('start')).to.equal('0'); + }); + }); + + describe('Infinite loading', () => { + beforeEach(() => { + // override rowCount + transformGetRowsResponse = (response) => ({ ...response, rowCount: -1 }); + }); + + it('should not render skeleton rows if rowCount is unknown', async () => { + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + // The 11th row should not exist + expect(() => getRow(10)).to.throw(); + }); + + it('should make a new data source request in infinite loading mode once the bottom row is reached', async () => { + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + // reset the spy call count + fetchRowsSpy.resetHistory(); + + // make one small and one big scroll that makes sure that the bottom of the grid window is reached + apiRef.current.scrollToIndexes({ rowIndex: 1 }); + apiRef.current.scrollToIndexes({ rowIndex: 9 }); + + // Only one additional fetch should have been made + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + }); + + it('should reset the scroll position when sorting is applied', async () => { + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + apiRef.current.scrollToIndexes({ rowIndex: 9 }); + + // wait until the rows are rendered + await waitFor(() => expect(getRow(10)).not.to.be.undefined); + + const beforeSortingSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + // last row is not the first page anymore + expect(beforeSortingSearchParams.get('end')).to.not.equal('9'); + + apiRef.current.sortColumn(mockServer.columns[0].field, 'asc'); + + const afterSortingSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + // last row is the end of the first page + expect(afterSortingSearchParams.get('end')).to.equal('9'); + }); + + it('should reset the scroll position when filter is applied', async () => { + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + apiRef.current.scrollToIndexes({ rowIndex: 9 }); + + // wait until the rows are rendered + await waitFor(() => expect(getRow(10)).not.to.be.undefined); + + const beforeFilteringSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + // last row is not the first page anymore + expect(beforeFilteringSearchParams.get('end')).to.not.equal('9'); + + apiRef.current.setFilterModel({ + items: [ + { + field: mockServer.columns[0].field, + value: '0', + operator: 'contains', + }, + ], + }); + + const afterFilteringSearchParams = new URL(fetchRowsSpy.lastCall.args[0]).searchParams; + // last row is the end of the first page + expect(afterFilteringSearchParams.get('end')).to.equal('9'); + }); + }); + + describe('Row count updates', () => { + it('should add skeleton rows once the rowCount becomes known', async () => { + // override rowCount + transformGetRowsResponse = (response) => ({ ...response, rowCount: undefined }); + const { setProps } = render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + // The 11th row should not exist + expect(() => getRow(10)).to.throw(); + + // make the rowCount known + setProps({ rowCount: 100 }); + + // The 11th row should be a skeleton + expect(getRow(10).dataset.id).to.equal('auto-generated-skeleton-row-root-10'); + }); + + it('should reset the grid if the rowCount becomes unknown', async () => { + // override rowCount + transformGetRowsResponse = (response) => ({ ...response, rowCount: undefined }); + const { setProps } = render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + // The 11th row should not exist + expect(getRow(10).dataset.id).to.equal('auto-generated-skeleton-row-root-10'); + + // make the rowCount unknown + setProps({ rowCount: -1 }); + + // The 11th row should not exist + expect(() => getRow(10)).to.throw(); + }); + + it('should reset the grid if the rowCount becomes smaller than the actual row count', async () => { + // override rowCount + transformGetRowsResponse = (response) => ({ ...response, rowCount: undefined }); + render( + , + ); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + // reset the spy call count + fetchRowsSpy.resetHistory(); + + // reduce the rowCount to be more than the number of rows + apiRef.current.setRowCount(80); + expect(fetchRowsSpy.callCount).to.equal(0); + + // reduce the rowCount once more, but now to be less than the number of rows + apiRef.current.setRowCount(20); + await waitFor(() => expect(fetchRowsSpy.callCount).to.equal(1)); + }); + + it('should allow setting the row count via API', async () => { + // override rowCount + transformGetRowsResponse = (response) => ({ ...response, rowCount: undefined }); + render(); + // wait until the rows are rendered + await waitFor(() => expect(getRow(0)).not.to.be.undefined); + + // The 11th row should not exist + expect(() => getRow(10)).to.throw(); + + // set the rowCount via API + apiRef.current.setRowCount(100); + + // wait until the rows are added + await waitFor(() => expect(getRow(10)).not.to.be.undefined); + // The 11th row should be a skeleton + expect(getRow(10).dataset.id).to.equal('auto-generated-skeleton-row-root-10'); + }); + }); +}); diff --git a/packages/x-data-grid-pro/src/typeOverloads/modules.ts b/packages/x-data-grid-pro/src/typeOverloads/modules.ts index 1d0fe7a306e6..430f21d777f4 100644 --- a/packages/x-data-grid-pro/src/typeOverloads/modules.ts +++ b/packages/x-data-grid-pro/src/typeOverloads/modules.ts @@ -42,6 +42,7 @@ export interface GridEventLookupPro { rowOrderChange: { params: GridRowOrderChangeParams }; /** * Fired when a new batch of rows is requested to be loaded. Called with a [[GridFetchRowsParams]] object. + * Used to trigger `onFetchRows`. */ fetchRows: { params: GridFetchRowsParams }; } diff --git a/packages/x-data-grid/src/hooks/core/strategyProcessing/gridStrategyProcessingApi.ts b/packages/x-data-grid/src/hooks/core/strategyProcessing/gridStrategyProcessingApi.ts index ef4094d3887f..3daf33790fe0 100644 --- a/packages/x-data-grid/src/hooks/core/strategyProcessing/gridStrategyProcessingApi.ts +++ b/packages/x-data-grid/src/hooks/core/strategyProcessing/gridStrategyProcessingApi.ts @@ -13,30 +13,48 @@ import { GridSortingMethodParams, GridSortingMethodValue, } from '../../features/sorting/gridSortingState'; +import { GridGetRowsParams, GridGetRowsResponse } from '../../../models/gridDataSource'; export type GridStrategyProcessorName = keyof GridStrategyProcessingLookup; -export type GridStrategyGroup = - GridStrategyProcessingLookup[keyof GridStrategyProcessingLookup]['group']; +export enum GridStrategyGroup { + DataSource = 'dataSource', + RowTree = 'rowTree', +} + +export type GridStrategyGroupValue = `${GridStrategyGroup}`; export interface GridStrategyProcessingLookup { + dataSourceRowsUpdate: { + group: GridStrategyGroup.DataSource; + params: + | { + response: GridGetRowsResponse; + fetchParams: GridGetRowsParams; + } + | { + error: Error; + fetchParams: GridGetRowsParams; + }; + value: void; + }; rowTreeCreation: { - group: 'rowTree'; + group: GridStrategyGroup.RowTree; params: GridRowTreeCreationParams; value: GridRowTreeCreationValue; }; filtering: { - group: 'rowTree'; + group: GridStrategyGroup.RowTree; params: GridFilteringMethodParams; value: GridFilteringMethodValue; }; sorting: { - group: 'rowTree'; + group: GridStrategyGroup.RowTree; params: GridSortingMethodParams; value: GridSortingMethodValue; }; visibleRowsLookupCreation: { - group: 'rowTree'; + group: GridStrategyGroup.RowTree; params: { tree: GridRowsState['tree']; filteredRowsLookup: GridFilterState['filteredRowsLookup']; @@ -66,21 +84,21 @@ export interface GridStrategyProcessingApi { ) => () => void; /** * Set a callback to know if a strategy is available. - * @param {GridStrategyGroup} strategyGroup The group for which we set strategy availability. + * @param {GridStrategyGroupValue} strategyGroup The group for which we set strategy availability. * @param {string} strategyName The name of the strategy. * @param {boolean} callback A callback to know if this strategy is available. */ setStrategyAvailability: ( - strategyGroup: GridStrategyGroup, + strategyGroup: GridStrategyGroupValue, strategyName: string, callback: () => boolean, ) => void; /** * Returns the name of the active strategy of a given strategy group - * @param {GridStrategyGroup} strategyGroup The group from which we want the active strategy. + * @param {GridStrategyGroupValue} strategyGroup The group from which we want the active strategy. * @returns {string} The name of the active strategy. */ - getActiveStrategy: (strategyGroup: GridStrategyGroup) => string; + getActiveStrategy: (strategyGroup: GridStrategyGroupValue) => string; /** * Run the processor registered for the active strategy. * @param {GridStrategyProcessorName} processorName The name of the processor to run. diff --git a/packages/x-data-grid/src/hooks/core/strategyProcessing/useGridStrategyProcessing.ts b/packages/x-data-grid/src/hooks/core/strategyProcessing/useGridStrategyProcessing.ts index 0dbe135f4d75..1fc83517a1fc 100644 --- a/packages/x-data-grid/src/hooks/core/strategyProcessing/useGridStrategyProcessing.ts +++ b/packages/x-data-grid/src/hooks/core/strategyProcessing/useGridStrategyProcessing.ts @@ -4,7 +4,7 @@ import { GridStrategyProcessor, GridStrategyProcessorName, GridStrategyProcessingApi, - GridStrategyProcessingLookup, + GridStrategyGroupValue, GridStrategyGroup, } from './gridStrategyProcessingApi'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; @@ -12,12 +12,13 @@ import { useGridApiMethod } from '../../utils/useGridApiMethod'; export const GRID_DEFAULT_STRATEGY = 'none'; export const GRID_STRATEGIES_PROCESSORS: { - [P in GridStrategyProcessorName]: GridStrategyProcessingLookup[P]['group']; + [P in GridStrategyProcessorName]: GridStrategyGroupValue; } = { - rowTreeCreation: 'rowTree', - filtering: 'rowTree', - sorting: 'rowTree', - visibleRowsLookupCreation: 'rowTree', + dataSourceRowsUpdate: GridStrategyGroup.DataSource, + rowTreeCreation: GridStrategyGroup.RowTree, + filtering: GridStrategyGroup.RowTree, + sorting: GridStrategyGroup.RowTree, + visibleRowsLookupCreation: GridStrategyGroup.RowTree, }; type UntypedStrategyProcessors = { @@ -59,14 +60,11 @@ type UntypedStrategyProcessors = { * ===================================================================================================================== * * Each processor name is part of a strategy group which can only have one active strategy at the time. - * For now, there is only one strategy group named `rowTree` which customize - * - row tree creation algorithm. - * - sorting algorithm. - * - filtering algorithm. + * There are two active groups named `rowTree` and `dataSource`. */ export const useGridStrategyProcessing = (apiRef: React.MutableRefObject) => { const availableStrategies = React.useRef( - new Map boolean }>(), + new Map boolean }>(), ); const strategiesCache = React.useRef<{ [P in GridStrategyProcessorName]?: { [strategyName: string]: GridStrategyProcessor }; diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRows.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRows.ts index bd89c281f729..d5f4a7cdf961 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRows.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRows.ts @@ -38,6 +38,7 @@ import { computeRowsUpdates, } from './gridRowsUtils'; import { useGridRegisterPipeApplier } from '../../core/pipeProcessing'; +import { GridStrategyGroup } from '../../core/strategyProcessing'; export const rowsStateInitializer: GridStateInitializer< Pick @@ -464,6 +465,8 @@ export const useGridRows = ( ...state, rows: { ...state.rows, + loading: props.loading, + totalRowCount: Math.max(props.rowCount || 0, rootGroupChildren.length), dataRowIdToModelLookup, dataRowIdToIdLookup, dataRowIds, @@ -472,7 +475,7 @@ export const useGridRows = ( })); apiRef.current.publishEvent('rowsSet'); }, - [apiRef, props.signature, props.getRowId], + [apiRef, props.signature, props.getRowId, props.loading, props.rowCount], ); const rowApi: GridRowApi = { @@ -559,7 +562,10 @@ export const useGridRows = ( >(() => { // `rowTreeCreation` is the only processor ran when `strategyAvailabilityChange` is fired. // All the other processors listen to `rowsSet` which will be published by the `groupRows` method below. - if (apiRef.current.getActiveStrategy('rowTree') !== gridRowGroupingNameSelector(apiRef)) { + if ( + apiRef.current.getActiveStrategy(GridStrategyGroup.RowTree) !== + gridRowGroupingNameSelector(apiRef) + ) { groupRows(); } }, [apiRef, groupRows]); @@ -621,8 +627,11 @@ export const useGridRows = ( lastRowCount.current = props.rowCount; } + const currentRows = props.unstable_dataSource + ? Array.from(apiRef.current.getRowModels().values()) + : props.rows; const areNewRowsAlreadyInState = - apiRef.current.caches.rows.rowsBeforePartialUpdates === props.rows; + apiRef.current.caches.rows.rowsBeforePartialUpdates === currentRows; const isNewLoadingAlreadyInState = apiRef.current.caches.rows.loadingPropBeforePartialUpdates === props.loading; const isNewRowCountAlreadyInState = @@ -657,10 +666,10 @@ export const useGridRows = ( } } - logger.debug(`Updating all rows, new length ${props.rows?.length}`); + logger.debug(`Updating all rows, new length ${currentRows?.length}`); throttledRowsChange({ cache: createRowsInternalCache({ - rows: props.rows, + rows: currentRows, getRowId: props.getRowId, loading: props.loading, rowCount: props.rowCount, @@ -672,6 +681,7 @@ export const useGridRows = ( props.rowCount, props.getRowId, props.loading, + props.unstable_dataSource, logger, throttledRowsChange, apiRef, diff --git a/packages/x-data-grid/src/internals/index.ts b/packages/x-data-grid/src/internals/index.ts index f12ed42b1a35..6988f682d169 100644 --- a/packages/x-data-grid/src/internals/index.ts +++ b/packages/x-data-grid/src/internals/index.ts @@ -17,6 +17,7 @@ export { getValueOptions } from '../components/panel/filterPanel/filterPanelUtil export { useGridRegisterPipeProcessor } from '../hooks/core/pipeProcessing'; export type { GridPipeProcessor } from '../hooks/core/pipeProcessing'; export { + GridStrategyGroup, useGridRegisterStrategyProcessor, GRID_DEFAULT_STRATEGY, } from '../hooks/core/strategyProcessing'; diff --git a/packages/x-data-grid/src/models/events/gridEventLookup.ts b/packages/x-data-grid/src/models/events/gridEventLookup.ts index b323518b8963..fd6628cfc071 100644 --- a/packages/x-data-grid/src/models/events/gridEventLookup.ts +++ b/packages/x-data-grid/src/models/events/gridEventLookup.ts @@ -389,6 +389,7 @@ export interface GridControlledStateReasonLookup { | 'restoreState' | 'removeAllFilterItems'; pagination: 'setPaginationModel' | 'stateRestorePreProcessing'; + rows: 'addSkeletonRows'; } export interface GridEventLookup diff --git a/packages/x-data-grid/src/models/gridDataSource.ts b/packages/x-data-grid/src/models/gridDataSource.ts index 2df17b84cf2d..75197e0afd1f 100644 --- a/packages/x-data-grid/src/models/gridDataSource.ts +++ b/packages/x-data-grid/src/models/gridDataSource.ts @@ -12,7 +12,7 @@ export interface GridGetRowsParams { /** * Alternate to `start` and `end`, maps to `GridPaginationModel` interface. */ - paginationModel: GridPaginationModel; + paginationModel?: GridPaginationModel; /** * First row index to fetch (number) or cursor information (number | string). */ @@ -20,7 +20,7 @@ export interface GridGetRowsParams { /** * Last row index to fetch. */ - end: number; // last row index to fetch + end: number; /** * List of grouped columns (only applicable with `rowGrouping`). */