Skip to content

Commit

Permalink
[DataGridPro] Implement Lazy loading (#5214)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanailH authored Sep 1, 2022
1 parent 604b05e commit bd6c15a
Show file tree
Hide file tree
Showing 54 changed files with 1,238 additions and 124 deletions.
15 changes: 15 additions & 0 deletions docs/data/data-grid/events/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@
"params": "GridEditRowsModel",
"event": "MuiEvent<{}>"
},
{
"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.",
"params": "GridFetchRowsParams",
"event": "MuiEvent<{}>",
"componentProp": "onFetchRows"
},
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "filterModelChange",
Expand Down Expand Up @@ -264,6 +272,13 @@
"event": "MuiEvent<{}>",
"componentProp": "onPreferencePanelOpen"
},
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "renderedRowsIntervalChange",
"description": "Fired when the rendered rows index interval changes. Called with a GridRenderedRowsIntervalChangeParams object.",
"params": "GridRenderedRowsIntervalChangeParams",
"event": "MuiEvent<{}>"
},
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "resize",
Expand Down
2 changes: 1 addition & 1 deletion docs/data/data-grid/row-updates/InfiniteLoadingGrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function InfiniteLoadingGrid() {

React.useEffect(() => {
return () => {
mounted.current = false;
mounted.current = true;
};
}, []);

Expand Down
2 changes: 1 addition & 1 deletion docs/data/data-grid/row-updates/InfiniteLoadingGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function InfiniteLoadingGrid() {

React.useEffect(() => {
return () => {
mounted.current = false;
mounted.current = true;
};
}, []);

Expand Down
106 changes: 106 additions & 0 deletions docs/data/data-grid/row-updates/LazyLoadingGrid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from 'react';
import { debounce } from '@mui/material/utils';
import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro';
import { createFakeServer, loadServerRows } from '@mui/x-data-grid-generator';

const DATASET_OPTION = {
dataSet: 'Employee',
rowLength: 10000,
};

const { columns, columnsWithDefaultColDef, useQuery } =
createFakeServer(DATASET_OPTION);

const emptyObject = {};

export default function LazyLoadingGrid() {
// dataServerSide simulates your database.
const { data: dataServerSide } = useQuery(emptyObject);

const apiRef = useGridApiRef();
const [initialRows, setInitialRows] = React.useState([]);
const [rowCount, setRowCount] = React.useState(0);

const fetchRow = React.useCallback(
async (params) => {
const serverRows = await loadServerRows(
dataServerSide,
{
filterModel: params.filterModel,
sortModel: params.sortModel,
},
{
minDelay: 300,
maxDelay: 800,
useCursorPagination: false,
},
columnsWithDefaultColDef,
);

return {
slice: serverRows.returnedRows.slice(
params.firstRowToRender,
params.lastRowToRender,
),
total: serverRows.returnedRows.length,
};
},
[dataServerSide],
);

// The initial fetch request of the viewport.
React.useEffect(() => {
if (dataServerSide.length === 0) {
return;
}

(async () => {
const { slice, total } = await fetchRow({
firstRowToRender: 0,
lastRowToRender: 10,
sortModel: [],
filterModel: {
items: [],
},
});

setInitialRows(slice);
setRowCount(total);
})();
}, [dataServerSide, fetchRow]);

// Fetch rows as they become visible in the viewport
const handleFetchRows = React.useCallback(
async (params) => {
const { slice, total } = await fetchRow(params);

apiRef.current.unstable_replaceRows(params.firstRowToRender, slice);
setRowCount(total);
},
[apiRef, fetchRow],
);

const debouncedHandleFetchRows = React.useMemo(
() => debounce(handleFetchRows, 200),
[handleFetchRows],
);

return (
<div style={{ height: 400, width: '100%' }}>
<DataGridPro
columns={columns}
rows={initialRows}
apiRef={apiRef}
hideFooterPagination
rowCount={rowCount}
sortingMode="server"
filterMode="server"
rowsLoadingMode="server"
onFetchRows={debouncedHandleFetchRows}
experimentalFeatures={{
lazyLoading: true,
}}
/>
</div>
);
}
114 changes: 114 additions & 0 deletions docs/data/data-grid/row-updates/LazyLoadingGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as React from 'react';
import { debounce } from '@mui/material/utils';
import {
DataGridPro,
GridFetchRowsParams,
useGridApiRef,
} from '@mui/x-data-grid-pro';
import {
createFakeServer,
loadServerRows,
UseDemoDataOptions,
} from '@mui/x-data-grid-generator';

const DATASET_OPTION: UseDemoDataOptions = {
dataSet: 'Employee',
rowLength: 10000,
};

const { columns, columnsWithDefaultColDef, useQuery } =
createFakeServer(DATASET_OPTION);

const emptyObject = {};

export default function LazyLoadingGrid() {
// dataServerSide simulates your database.
const { data: dataServerSide } = useQuery(emptyObject);

const apiRef = useGridApiRef();
const [initialRows, setInitialRows] = React.useState<typeof dataServerSide>([]);
const [rowCount, setRowCount] = React.useState(0);

const fetchRow = React.useCallback(
async (params: GridFetchRowsParams) => {
const serverRows = await loadServerRows(
dataServerSide,
{
filterModel: params.filterModel,
sortModel: params.sortModel,
},
{
minDelay: 300,
maxDelay: 800,
useCursorPagination: false,
},
columnsWithDefaultColDef,
);

return {
slice: serverRows.returnedRows.slice(
params.firstRowToRender,
params.lastRowToRender,
),
total: serverRows.returnedRows.length,
};
},
[dataServerSide],
);

// The initial fetch request of the viewport.
React.useEffect(() => {
if (dataServerSide.length === 0) {
return;
}

(async () => {
const { slice, total } = await fetchRow({
firstRowToRender: 0,
lastRowToRender: 10,
sortModel: [],
filterModel: {
items: [],
},
});

setInitialRows(slice);
setRowCount(total);
})();
}, [dataServerSide, fetchRow]);

// Fetch rows as they become visible in the viewport
const handleFetchRows = React.useCallback(
async (params: GridFetchRowsParams) => {
const { slice, total } = await fetchRow(params);

apiRef.current.unstable_replaceRows(params.firstRowToRender, slice);
setRowCount(total);
},
[apiRef, fetchRow],
);

const debouncedHandleFetchRows = React.useMemo(
() => debounce(handleFetchRows, 200),
[handleFetchRows],
);

return (
<div style={{ height: 400, width: '100%' }}>
<DataGridPro
columns={columns}
rows={initialRows}
apiRef={apiRef}
hideFooterPagination
rowCount={rowCount}
sortingMode="server"
filterMode="server"
rowsLoadingMode="server"
onFetchRows={debouncedHandleFetchRows}
experimentalFeatures={{
lazyLoading: true,
}}
/>
</div>
);
}
14 changes: 14 additions & 0 deletions docs/data/data-grid/row-updates/LazyLoadingGrid.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<DataGridPro
columns={columns}
rows={initialRows}
apiRef={apiRef}
hideFooterPagination
rowCount={rowCount}
sortingMode="server"
filterMode="server"
rowsLoadingMode="server"
onFetchRows={debouncedHandleFetchRows}
experimentalFeatures={{
lazyLoading: true,
}}
/>
34 changes: 34 additions & 0 deletions docs/data/data-grid/row-updates/row-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,40 @@ In addition, the area in which `onRowsScrollEnd` is called can be changed using

{{"demo": "InfiniteLoadingGrid.js", "bg": "inline", "disableAd": true}}

## Lazy loading [<span class="plan-pro"></span>](/x/introduction/licensing/#pro-plan)

:::warning
This feature is experimental, it needs to be explicitly activated using the `lazyLoading` experimental feature flag.

```tsx
<DataGridPro experimentalFeatures={{ lazyLoading: true }} {...otherProps} />
```

:::

Lazy Loading works like a pagination system, but instead of loading new rows based on pages, it loads them based on the viewport.
It loads new rows in chunks, as the user scrolls through the grid and reveals empty rows.

The data grid builds the vertical scroll as if all the rows are already there, and displays empty (skeleton) rows while loading the data. Only rows that are displayed get fetched.

To enable lazy loading, there are a few steps you need to follow:

First, set `rowsLoadingMode="server"`.
Then, set `rowCount` to reflect the number of available rows on the server.
Third, set a callback function on `onFetchRows` to load the data corresponding to the row indices passed within `GridFetchRowsParams`.
Finally, replace the empty rows with the newly fetched ones using `apiRef.current.replaceRows()` like in the demo below.

{{"demo": "LazyLoadingGrid.js", "bg": "inline", "disableAd": true}}

:::warning
The `onFetchRows` callback is called every time a new row is in the viewport, so when you scroll, you can easily send multiple requests to your backend. We recommend developers limit those by implementing debouncing.
:::

:::info
In order for filtering and sorting to work you need to set their modes to `server`.
You can find out more information about how to do that on the [server-side filter page](/x/react-data-grid/filtering/#server-side-filter) and on the [server-side sorting page](/x/react-data-grid/sorting/#server-side-sorting).
:::

## High frequency [<span class="plan-pro"></span>](/x/introduction/licensing/#pro-plan)

Whenever the rows are updated, the grid has to apply the sorting and filters. This can be a problem if you have high frequency updates. To maintain good performances, the grid allows to batch the updates and only apply them after a period of time. The `throttleRowsMs` prop can be used to define the frequency (in milliseconds) at which rows updates are applied.
Expand Down
7 changes: 6 additions & 1 deletion docs/pages/x/api/data-grid/data-grid-premium.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"experimentalFeatures": {
"type": {
"name": "shape",
"description": "{ aggregation?: bool, columnGrouping?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }"
"description": "{ aggregation?: bool, columnGrouping?: bool, lazyLoading?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }"
}
},
"filterMode": {
Expand Down Expand Up @@ -159,6 +159,7 @@
},
"onEditRowsModelChange": { "type": { "name": "func" } },
"onError": { "type": { "name": "func" } },
"onFetchRows": { "type": { "name": "func" } },
"onFilterModelChange": { "type": { "name": "func" } },
"onMenuClose": { "type": { "name": "func" } },
"onMenuOpen": { "type": { "name": "func" } },
Expand Down Expand Up @@ -205,6 +206,9 @@
"rowHeight": { "type": { "name": "number" }, "default": "52" },
"rowModesModel": { "type": { "name": "object" } },
"rowReordering": { "type": { "name": "bool" } },
"rowsLoadingMode": {
"type": { "name": "enum", "description": "'client'<br>&#124;&nbsp;'server'" }
},
"rowSpacingType": {
"type": { "name": "enum", "description": "'border'<br>&#124;&nbsp;'margin'" },
"default": "\"margin\""
Expand Down Expand Up @@ -342,6 +346,7 @@
"cell",
"cellContent",
"cellCheckbox",
"cellSkeleton",
"checkboxInput",
"columnHeader--alignCenter",
"columnHeader--alignLeft",
Expand Down
Loading

0 comments on commit bd6c15a

Please sign in to comment.