Skip to content

Commit fc6ab97

Browse files
authored
[Data Grid] Add recipe for cursor pagination with data source (#19700)
1 parent 2423e2f commit fc6ab97

File tree

6 files changed

+515
-0
lines changed

6 files changed

+515
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as React from 'react';
2+
import { DataGrid } from '@mui/x-data-grid';
3+
import { useMockServer } from '@mui/x-data-grid-generator';
4+
5+
export default function ServerSideCursorBlocking() {
6+
const { columns, initialState, fetchRows } = useMockServer(
7+
{},
8+
{ useCursorPagination: true, minDelay: 1000, maxDelay: 2000 },
9+
);
10+
const [paginationModel, setPaginationModel] = React.useState({
11+
page: 0,
12+
pageSize: 10,
13+
});
14+
const paginationModelRef = React.useRef(paginationModel);
15+
paginationModelRef.current = paginationModel;
16+
17+
const mapPageToNextCursor = React.useRef({});
18+
19+
const handlePaginationModelChange = (newPaginationModel) => {
20+
if (
21+
newPaginationModel.page === 0 ||
22+
mapPageToNextCursor.current[newPaginationModel.page - 1]
23+
) {
24+
setPaginationModel(newPaginationModel);
25+
}
26+
};
27+
28+
const dataSource = React.useMemo(
29+
() => ({
30+
getRows: async (params) => {
31+
const cursor =
32+
mapPageToNextCursor.current[paginationModelRef.current.page - 1];
33+
const urlParams = new URLSearchParams({
34+
paginationModel: JSON.stringify(params.paginationModel),
35+
filterModel: JSON.stringify(params.filterModel),
36+
sortModel: JSON.stringify(params.sortModel),
37+
cursor: String(cursor ?? ''),
38+
});
39+
const getRowsResponse = await fetchRows(
40+
`https://mui.com/x/api/data-grid?${urlParams.toString()}`,
41+
);
42+
mapPageToNextCursor.current[params.paginationModel?.page || 0] =
43+
getRowsResponse.pageInfo?.nextCursor;
44+
return {
45+
rows: getRowsResponse.rows,
46+
rowCount: getRowsResponse.rowCount,
47+
};
48+
},
49+
}),
50+
[fetchRows],
51+
);
52+
53+
return (
54+
<div style={{ width: '100%', height: 400 }}>
55+
<DataGrid
56+
columns={columns}
57+
dataSource={dataSource}
58+
pagination
59+
initialState={{
60+
...initialState,
61+
pagination: { rowCount: 0 },
62+
}}
63+
paginationModel={paginationModel}
64+
onPaginationModelChange={handlePaginationModelChange}
65+
pageSizeOptions={[10, 20, 50]}
66+
/>
67+
</div>
68+
);
69+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as React from 'react';
2+
import {
3+
DataGrid,
4+
GridDataSource,
5+
GridPaginationModel,
6+
GridRowId,
7+
} from '@mui/x-data-grid';
8+
import { useMockServer } from '@mui/x-data-grid-generator';
9+
10+
export default function ServerSideCursorBlocking() {
11+
const { columns, initialState, fetchRows } = useMockServer(
12+
{},
13+
{ useCursorPagination: true, minDelay: 1000, maxDelay: 2000 },
14+
);
15+
const [paginationModel, setPaginationModel] = React.useState({
16+
page: 0,
17+
pageSize: 10,
18+
});
19+
const paginationModelRef = React.useRef(paginationModel);
20+
paginationModelRef.current = paginationModel;
21+
22+
const mapPageToNextCursor = React.useRef<{
23+
[page: number]: GridRowId | undefined;
24+
}>({});
25+
26+
const handlePaginationModelChange = (newPaginationModel: GridPaginationModel) => {
27+
if (
28+
newPaginationModel.page === 0 ||
29+
mapPageToNextCursor.current[newPaginationModel.page - 1]
30+
) {
31+
setPaginationModel(newPaginationModel);
32+
}
33+
};
34+
35+
const dataSource: GridDataSource = React.useMemo(
36+
() => ({
37+
getRows: async (params) => {
38+
const cursor =
39+
mapPageToNextCursor.current[paginationModelRef.current.page - 1];
40+
const urlParams = new URLSearchParams({
41+
paginationModel: JSON.stringify(params.paginationModel),
42+
filterModel: JSON.stringify(params.filterModel),
43+
sortModel: JSON.stringify(params.sortModel),
44+
cursor: String(cursor ?? ''),
45+
});
46+
const getRowsResponse = await fetchRows(
47+
`https://mui.com/x/api/data-grid?${urlParams.toString()}`,
48+
);
49+
mapPageToNextCursor.current[params.paginationModel?.page || 0] =
50+
getRowsResponse.pageInfo?.nextCursor;
51+
return {
52+
rows: getRowsResponse.rows,
53+
rowCount: getRowsResponse.rowCount,
54+
};
55+
},
56+
}),
57+
[fetchRows],
58+
);
59+
60+
return (
61+
<div style={{ width: '100%', height: 400 }}>
62+
<DataGrid
63+
columns={columns}
64+
dataSource={dataSource}
65+
pagination
66+
initialState={{
67+
...initialState,
68+
pagination: { rowCount: 0 },
69+
}}
70+
paginationModel={paginationModel}
71+
onPaginationModelChange={handlePaginationModelChange}
72+
pageSizeOptions={[10, 20, 50]}
73+
/>
74+
</div>
75+
);
76+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<DataGrid
2+
columns={columns}
3+
dataSource={dataSource}
4+
pagination
5+
initialState={{
6+
...initialState,
7+
pagination: { rowCount: 0 },
8+
}}
9+
paginationModel={paginationModel}
10+
onPaginationModelChange={handlePaginationModelChange}
11+
pageSizeOptions={[10, 20, 50]}
12+
/>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import * as React from 'react';
2+
import { DataGrid, useGridApiRef } from '@mui/x-data-grid';
3+
import Snackbar from '@mui/material/Snackbar';
4+
import Alert from '@mui/material/Alert';
5+
import { useMockServer } from '@mui/x-data-grid-generator';
6+
7+
function getKeyDefault(params) {
8+
return JSON.stringify([
9+
params.filterModel,
10+
params.sortModel,
11+
params.start,
12+
params.end,
13+
]);
14+
}
15+
16+
class Cache {
17+
constructor() {
18+
this.cache = {};
19+
this.cacheKeys = new Set();
20+
this.getKey = getKeyDefault;
21+
}
22+
23+
set(key, value) {
24+
const keyString = this.getKey(key);
25+
this.cache[keyString] = { value };
26+
}
27+
28+
get(key) {
29+
const keyString = this.getKey(key);
30+
const entry = this.cache[keyString];
31+
if (!entry) {
32+
return undefined;
33+
}
34+
35+
return entry.value;
36+
}
37+
38+
pushKey(key) {
39+
const keyString = this.getKey(key);
40+
this.cacheKeys.add(keyString);
41+
}
42+
43+
deleteKey(key) {
44+
const keyString = this.getKey(key);
45+
delete this.cache[keyString];
46+
this.cacheKeys.delete(keyString);
47+
}
48+
49+
async getLast(key) {
50+
const cacheKeys = Array.from(this.cacheKeys);
51+
const prevKey = cacheKeys[cacheKeys.indexOf(this.getKey(key)) - 1];
52+
if (!prevKey) {
53+
return undefined;
54+
}
55+
if (this.cache[prevKey]) {
56+
return this.cache[prevKey].value;
57+
}
58+
return new Promise((resolve) => {
59+
const intervalId = setInterval(() => {
60+
if (this.cache[prevKey]) {
61+
clearInterval(intervalId);
62+
resolve(this.cache[prevKey].value);
63+
}
64+
}, 100);
65+
});
66+
}
67+
68+
clear() {
69+
this.cache = {};
70+
this.cacheKeys.clear();
71+
}
72+
}
73+
74+
export default function ServerSideCursorNonBlocking() {
75+
const apiRef = useGridApiRef();
76+
const [snackbar, setSnackbar] = React.useState(null);
77+
const { columns, initialState, fetchRows } = useMockServer(
78+
{},
79+
{ useCursorPagination: true, minDelay: 200, maxDelay: 500 },
80+
);
81+
const cache = React.useMemo(() => new Cache(), []);
82+
83+
const dataSource = React.useMemo(
84+
() => ({
85+
getRows: async (params) => {
86+
cache.pushKey(params);
87+
const latestResponse = await cache.getLast(params);
88+
const urlParams = new URLSearchParams({
89+
paginationModel: JSON.stringify(params.paginationModel),
90+
filterModel: JSON.stringify(params.filterModel),
91+
sortModel: JSON.stringify(params.sortModel),
92+
cursor: String(latestResponse?.pageInfo?.nextCursor ?? ''),
93+
});
94+
try {
95+
if (params.paginationModel?.page === 7) {
96+
throw new Error('Simulate server error on page 8');
97+
}
98+
const getRowsResponse = await fetchRows(
99+
`https://mui.com/x/api/data-grid?${urlParams.toString()}`,
100+
);
101+
return {
102+
rows: getRowsResponse.rows,
103+
rowCount: getRowsResponse.rowCount,
104+
pageInfo: { nextCursor: getRowsResponse.pageInfo?.nextCursor },
105+
};
106+
} catch (error) {
107+
cache.deleteKey(params);
108+
apiRef.current?.setPaginationModel({
109+
page: params.paginationModel.page - 1,
110+
pageSize: params.paginationModel.pageSize,
111+
});
112+
setSnackbar({ children: error.message, severity: 'error' });
113+
throw error;
114+
}
115+
},
116+
}),
117+
[fetchRows, cache, apiRef],
118+
);
119+
120+
const handleCloseSnackbar = () => {
121+
setSnackbar(null);
122+
};
123+
124+
return (
125+
<div style={{ width: '100%', height: 400 }}>
126+
<DataGrid
127+
apiRef={apiRef}
128+
columns={columns}
129+
dataSource={dataSource}
130+
dataSourceCache={cache}
131+
pagination
132+
initialState={{
133+
...initialState,
134+
pagination: {
135+
paginationModel: {
136+
page: 0,
137+
pageSize: 10,
138+
},
139+
rowCount: 0,
140+
},
141+
}}
142+
pageSizeOptions={[10, 20, 50]}
143+
onFilterModelChange={() => cache.clear()}
144+
onSortModelChange={() => cache.clear()}
145+
/>
146+
{!!snackbar && (
147+
<Snackbar
148+
open
149+
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
150+
onClose={handleCloseSnackbar}
151+
autoHideDuration={6000}
152+
>
153+
<Alert {...snackbar} onClose={handleCloseSnackbar} />
154+
</Snackbar>
155+
)}
156+
</div>
157+
);
158+
}

0 commit comments

Comments
 (0)