From 4aa33fd7d7a8d6a9e15aff5818b80ffcc6a7acd8 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 18 Mar 2024 12:42:27 +0500 Subject: [PATCH 01/27] [DataGrid] Support advanced server side pagination use cases --- .../pagination/CursorPaginationGrid.js | 119 ++++++++++++---- .../pagination/CursorPaginationGrid.tsx | 128 ++++++++++++++---- .../ServerPaginationGridNoRowCount.js | 55 ++++++++ .../ServerPaginationGridNoRowCount.tsx | 58 ++++++++ ...erverPaginationGridNoRowCount.tsx.preview} | 9 +- .../ServerPaginationGridTruncated.js | 63 +++++++++ .../ServerPaginationGridTruncated.tsx | 66 +++++++++ .../ServerPaginationGridTruncated.tsx.preview | 16 +++ docs/data/data-grid/pagination/pagination.md | 73 +++++++++- .../src/hooks/useQuery.ts | 7 +- .../src/DataGridPremium/DataGridPremium.tsx | 19 +++ .../src/DataGridPro/DataGridPro.tsx | 19 +++ .../x-data-grid/src/DataGrid/DataGrid.tsx | 19 +++ .../src/components/GridPagination.tsx | 51 ++++++- .../pagination/gridPaginationInterfaces.ts | 20 ++- .../pagination/gridPaginationSelector.ts | 14 +- .../pagination/gridPaginationUtils.ts | 6 +- .../features/pagination/useGridPagination.ts | 11 +- .../pagination/useGridPaginationMeta.ts | 113 ++++++++++++++++ .../pagination/useGridPaginationModel.ts | 9 +- .../features/pagination/useGridRowCount.ts | 92 +++++++++++-- .../src/internals/utils/propValidation.ts | 8 ++ .../src/models/events/gridEventLookup.ts | 6 +- .../src/models/gridPaginationProps.ts | 4 + .../src/models/props/DataGridProps.ts | 19 ++- 25 files changed, 920 insertions(+), 84 deletions(-) create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx rename docs/data/data-grid/pagination/{CursorPaginationGrid.tsx.preview => ServerPaginationGridNoRowCount.tsx.preview} (55%) create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridTruncated.js create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridTruncated.tsx create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridTruncated.tsx.preview create mode 100644 packages/x-data-grid/src/hooks/features/pagination/useGridPaginationMeta.ts diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.js b/docs/data/data-grid/pagination/CursorPaginationGrid.js index cf369037fa5f..629081742a55 100644 --- a/docs/data/data-grid/pagination/CursorPaginationGrid.js +++ b/docs/data/data-grid/pagination/CursorPaginationGrid.js @@ -1,5 +1,10 @@ import * as React from 'react'; import { DataGrid } from '@mui/x-data-grid'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; import { createFakeServer } from '@mui/x-data-grid-generator'; const PAGE_SIZE = 5; @@ -11,6 +16,8 @@ const SERVER_OPTIONS = { const { useQuery, ...data } = createFakeServer({}, SERVER_OPTIONS); export default function CursorPaginationGrid() { + const [rowCountType, setRowCountType] = React.useState('known'); + const mapPageToNextCursor = React.useRef({}); const [paginationModel, setPaginationModel] = React.useState({ @@ -25,7 +32,11 @@ export default function CursorPaginationGrid() { }), [paginationModel], ); - const { isLoading, rows, pageInfo } = useQuery(queryOptions); + const { + isLoading, + rows, + pageInfo: { hasNextPage, nextCursor, totalRowCount }, + } = useQuery(queryOptions); const handlePaginationModelChange = (newPaginationModel) => { // We have the cursor, we can allow the page transition. @@ -37,38 +48,98 @@ export default function CursorPaginationGrid() { } }; + const paginationMetaRef = React.useRef(); + + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + + React.useEffect(() => { + console.log(paginationMeta); + }, [paginationMeta]); + React.useEffect(() => { - if (!isLoading && pageInfo?.nextCursor) { + if (!isLoading && nextCursor) { // We add nextCursor when available - mapPageToNextCursor.current[paginationModel.page] = pageInfo?.nextCursor; + mapPageToNextCursor.current[paginationModel.page] = nextCursor; } - }, [paginationModel.page, isLoading, pageInfo?.nextCursor]); + }, [paginationModel.page, isLoading, nextCursor]); // Some API clients return undefined while loading // Following lines are here to prevent `rowCountState` from being undefined during the loading - const [rowCountState, setRowCountState] = React.useState( - pageInfo?.totalRowCount || 0, - ); + const [rowCountState, setRowCountState] = React.useState(totalRowCount || 0); React.useEffect(() => { - setRowCountState((prevRowCountState) => - pageInfo?.totalRowCount !== undefined - ? pageInfo?.totalRowCount - : prevRowCountState, - ); - }, [pageInfo?.totalRowCount, setRowCountState]); + if (rowCountType === 'known') { + setRowCountState((prevRowCountState) => + totalRowCount !== undefined ? totalRowCount : prevRowCountState, + ); + } + if ( + (rowCountType === 'unknown' || rowCountType === 'estimated') && + paginationMeta?.hasNextPage !== false + ) { + setRowCountState(-1); + } + }, [paginationMeta?.hasNextPage, rowCountType, totalRowCount]); + + const prevEstimatedRowCount = React.useRef(undefined); + const estimatedRowCount = React.useMemo(() => { + if (rowCountType === 'estimated') { + if (totalRowCount !== undefined) { + if (prevEstimatedRowCount.current === undefined) { + prevEstimatedRowCount.current = totalRowCount / 2; + } + return totalRowCount / 2; + } + return prevEstimatedRowCount.current; + } + return undefined; + }, [rowCountType, totalRowCount]); return ( -
- +
+ + + Row count + + setRowCountType(e.target.value)} + > + } label="Known" /> + } label="Unknown" /> + } + label="Estimated" + /> + + +
+ setRowCountState(newRowCount)} + estimatedRowCount={estimatedRowCount} + paginationMeta={paginationMeta} + paginationMode="server" + onPaginationModelChange={handlePaginationModelChange} + paginationModel={paginationModel} + loading={isLoading} + /> +
); } diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx index 5aff7d9779f2..716e2782f38a 100644 --- a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx +++ b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx @@ -1,5 +1,15 @@ import * as React from 'react'; -import { DataGrid, GridRowId, GridPaginationModel } from '@mui/x-data-grid'; +import { + DataGrid, + GridRowId, + GridPaginationModel, + GridPaginationMeta, +} from '@mui/x-data-grid'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; import { createFakeServer } from '@mui/x-data-grid-generator'; const PAGE_SIZE = 5; @@ -10,7 +20,11 @@ const SERVER_OPTIONS = { const { useQuery, ...data } = createFakeServer({}, SERVER_OPTIONS); +type RowCountType = 'known' | 'unknown' | 'estimated'; + export default function CursorPaginationGrid() { + const [rowCountType, setRowCountType] = React.useState('known'); + const mapPageToNextCursor = React.useRef<{ [page: number]: GridRowId }>({}); const [paginationModel, setPaginationModel] = React.useState({ @@ -25,7 +39,11 @@ export default function CursorPaginationGrid() { }), [paginationModel], ); - const { isLoading, rows, pageInfo } = useQuery(queryOptions); + const { + isLoading, + rows, + pageInfo: { hasNextPage, nextCursor, totalRowCount }, + } = useQuery(queryOptions); const handlePaginationModelChange = (newPaginationModel: GridPaginationModel) => { // We have the cursor, we can allow the page transition. @@ -37,38 +55,98 @@ export default function CursorPaginationGrid() { } }; + const paginationMetaRef = React.useRef(); + + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + + React.useEffect(() => { + console.log(paginationMeta); + }, [paginationMeta]); + React.useEffect(() => { - if (!isLoading && pageInfo?.nextCursor) { + if (!isLoading && nextCursor) { // We add nextCursor when available - mapPageToNextCursor.current[paginationModel.page] = pageInfo?.nextCursor; + mapPageToNextCursor.current[paginationModel.page] = nextCursor; } - }, [paginationModel.page, isLoading, pageInfo?.nextCursor]); + }, [paginationModel.page, isLoading, nextCursor]); // Some API clients return undefined while loading // Following lines are here to prevent `rowCountState` from being undefined during the loading - const [rowCountState, setRowCountState] = React.useState( - pageInfo?.totalRowCount || 0, - ); + const [rowCountState, setRowCountState] = React.useState(totalRowCount || 0); React.useEffect(() => { - setRowCountState((prevRowCountState) => - pageInfo?.totalRowCount !== undefined - ? pageInfo?.totalRowCount - : prevRowCountState, - ); - }, [pageInfo?.totalRowCount, setRowCountState]); + if (rowCountType === 'known') { + setRowCountState((prevRowCountState) => + totalRowCount !== undefined ? totalRowCount : prevRowCountState, + ); + } + if ( + (rowCountType === 'unknown' || rowCountType === 'estimated') && + paginationMeta?.hasNextPage !== false + ) { + setRowCountState(-1); + } + }, [paginationMeta?.hasNextPage, rowCountType, totalRowCount]); + + const prevEstimatedRowCount = React.useRef(undefined); + const estimatedRowCount = React.useMemo(() => { + if (rowCountType === 'estimated') { + if (totalRowCount !== undefined) { + if (prevEstimatedRowCount.current === undefined) { + prevEstimatedRowCount.current = totalRowCount / 2; + } + return totalRowCount / 2; + } + return prevEstimatedRowCount.current; + } + return undefined; + }, [rowCountType, totalRowCount]); return ( -
- +
+ + + Row count + + setRowCountType(e.target.value as RowCountType)} + > + } label="Known" /> + } label="Unknown" /> + } + label="Estimated" + /> + + +
+ setRowCountState(newRowCount)} + estimatedRowCount={estimatedRowCount} + paginationMeta={paginationMeta} + paginationMode="server" + onPaginationModelChange={handlePaginationModelChange} + paginationModel={paginationModel} + loading={isLoading} + /> +
); } diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js new file mode 100644 index 000000000000..b86536250190 --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { DataGrid } from '@mui/x-data-grid'; +import { createFakeServer } from '@mui/x-data-grid-generator'; + +const SERVER_OPTIONS = { + useCursorPagination: false, +}; + +const { useQuery, ...data } = createFakeServer({}, SERVER_OPTIONS); + +export default function ServerPaginationGridNoRowCount() { + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize: 5, + }); + + const { + isLoading, + rows, + pageInfo: { hasNextPage }, + } = useQuery(paginationModel); + + const paginationMetaRef = React.useRef(); + + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + + const handlePaginationModelChange = React.useCallback((newPaginationModel) => { + setPaginationModel(newPaginationModel); + }, []); + + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx new file mode 100644 index 000000000000..f64716ab31d3 --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { DataGrid, GridPaginationMeta, GridPaginationModel } from '@mui/x-data-grid'; +import { createFakeServer } from '@mui/x-data-grid-generator'; + +const SERVER_OPTIONS = { + useCursorPagination: false, +}; + +const { useQuery, ...data } = createFakeServer({}, SERVER_OPTIONS); + +export default function ServerPaginationGridNoRowCount() { + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize: 5, + }); + + const { + isLoading, + rows, + pageInfo: { hasNextPage }, + } = useQuery(paginationModel); + + const paginationMetaRef = React.useRef(); + + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + + const handlePaginationModelChange = React.useCallback( + (newPaginationModel: GridPaginationModel) => { + setPaginationModel(newPaginationModel); + }, + [], + ); + + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx.preview b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview similarity index 55% rename from docs/data/data-grid/pagination/CursorPaginationGrid.tsx.preview rename to docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview index 32c24513f995..6d8e1a16174a 100644 --- a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx.preview +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview @@ -1,10 +1,11 @@ \ No newline at end of file diff --git a/docs/data/data-grid/pagination/ServerPaginationGridTruncated.js b/docs/data/data-grid/pagination/ServerPaginationGridTruncated.js new file mode 100644 index 000000000000..05def6f6232d --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridTruncated.js @@ -0,0 +1,63 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import { DataGrid, useGridApiRef } from '@mui/x-data-grid'; + +import { createFakeServer } from '@mui/x-data-grid-generator'; + +const SERVER_OPTIONS = { + useCursorPagination: false, +}; + +const { useQuery, ...data } = createFakeServer({ rowLength: 1000 }, SERVER_OPTIONS); + +export default function ServerPaginationGridTruncated() { + const apiRef = useGridApiRef(); + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize: 50, + }); + + const { + isLoading, + rows, + pageInfo: { hasNextPage }, + } = useQuery(paginationModel); + + const [paginationMeta, setPaginationMeta] = React.useState({}); + + React.useEffect(() => { + if (hasNextPage !== undefined) { + setPaginationMeta((prev) => { + if (prev.hasNextPage !== hasNextPage) { + return { ...prev, hasNextPage }; + } + return prev; + }); + } + }, [hasNextPage]); + + const handlePaginationModelChange = React.useCallback((newPaginationModel) => { + setPaginationModel(newPaginationModel); + }, []); + + return ( +
+ +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/pagination/ServerPaginationGridTruncated.tsx b/docs/data/data-grid/pagination/ServerPaginationGridTruncated.tsx new file mode 100644 index 000000000000..060004b66184 --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridTruncated.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import { DataGrid, useGridApiRef } from '@mui/x-data-grid'; +import type { GridPaginationMeta, GridPaginationModel } from '@mui/x-data-grid'; +import { createFakeServer } from '@mui/x-data-grid-generator'; + +const SERVER_OPTIONS = { + useCursorPagination: false, +}; + +const { useQuery, ...data } = createFakeServer({ rowLength: 1000 }, SERVER_OPTIONS); + +export default function ServerPaginationGridTruncated() { + const apiRef = useGridApiRef(); + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize: 50, + }); + + const { + isLoading, + rows, + pageInfo: { hasNextPage }, + } = useQuery(paginationModel); + + const [paginationMeta, setPaginationMeta] = React.useState({}); + + React.useEffect(() => { + if (hasNextPage !== undefined) { + setPaginationMeta((prev) => { + if (prev.hasNextPage !== hasNextPage) { + return { ...prev, hasNextPage }; + } + return prev; + }); + } + }, [hasNextPage]); + + const handlePaginationModelChange = React.useCallback( + (newPaginationModel: GridPaginationModel) => { + setPaginationModel(newPaginationModel); + }, + [], + ); + + return ( +
+ +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/pagination/ServerPaginationGridTruncated.tsx.preview b/docs/data/data-grid/pagination/ServerPaginationGridTruncated.tsx.preview new file mode 100644 index 000000000000..43b0a1431e76 --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridTruncated.tsx.preview @@ -0,0 +1,16 @@ + +
+ +
\ No newline at end of file diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index eebea559626f..ef26207384ee 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -96,14 +96,33 @@ By default, the pagination is handled on the client. This means you have to give the rows of all pages to the data grid. If your dataset is too big, and you only want to fetch the current page, you can use server-side pagination. +Grid supports two types of server-side pagination based on the supported API from the server: + +- Index-based pagination +- Cursor-based pagination + :::info Check out [Selection—Usage with server-side pagination](/x/react-data-grid/row-selection/#usage-with-server-side-pagination) for more details. ::: -### Basic implementation +### Index-based pagination + +The index-based pagination is the most common type of pagination. It follows the same pattern as the client-side pagination (page, pageSize), but the data is fetched from the server. + +It can be further categorized into sub-types based on the availability of the total number of rows or `rowCount`. There can three different possibilities: + +1. Row count is available +2. Row count is not available +3. Row count is available but is not accurate and may update later on + +In the following sections, we will discuss how to handle each of these cases. + +#### 1. Row count is available + +When the row count is available, you can use the following steps to enable server-side pagination: - Set the prop `paginationMode` to `server` -- Provide a `rowCount` prop to let the data grid know how many pages there are +- Provide the `rowCount` using the `initialState.pagination.rowCount` or the `rowCount` prop to let the data grid calculate how many pages are there - Use the `onPaginationModelChange` prop callback to load the rows when the page changes Since the `rowCount` prop is used to compute the number of available pages, switching it to `undefined` during loading resets the page to zero. @@ -122,6 +141,56 @@ React.useEffect(() => { {{"demo": "ServerPaginationGrid.js", "bg": "inline"}} +#### 2. Row count is not available + +When the row count is not available, you can use the following steps to enable server-side pagination: + +- Set the prop `paginationMode` to `server` +- Use the `onPaginationModelChange` prop callback to load the rows when the page changes +- Pass the initial `rowCount` using the `initialState.pagination.rowCount` or the `rowCount` prop as `-1` to let the Grid know that the row count is not available +- Since the `rowCount` is unknown, the Grid needs to know whether more records are available on the server. You can use the `paginationMeta.hasNextPage` property to indicate whether more records are available. + +The `hasNextPage` must not be set to `false` until there are actually no records left to fetch, because when `hasNextPage` becomes false, the Grid sets the `rowCount` to the currently fetched rows to switch back to the known rows case. + +The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook, so you can memoize the `paginationMeta` value to avoid unnecessary flickering of the pagination UI: + +```tsx +const paginationMetaRef = React.useRef(); + +const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; +}, [hasNextPage]); +``` + +{{"demo": "ServerPaginationGridNoRowCount.js", "bg": "inline"}} + +#### 3. Row count is available but is not accurate + +There could be possibilities when the accurate row count is not initially available, either because computing it upfront is a costly operation or it could not be known until the last page is fetched. In such cases, the backend could send an estimated row count which could be initially used to calculate the number of pages. Pass the estimated row count using the `estimatedRowCount` prop and set the `rowCount` to `-1` to indicate that the actual row count is not (yet) available. + +There could be two further possibilities: + +1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop, either by updating the `rowCount` prop or using the `setRowCount` method of the `apiRef`. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a value >= 0. +2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage` prop to indicate whether more records are available on the server. + +Here are the steps to achieve it: + +- Set the prop `paginationMode` to `server` +- Use the `onPaginationModelChange` prop callback to load the rows when the page changes +- Pass the initial estimated `rowCount` using the `estimatedRowCount` prop and set the `rowCount` as `-1` (in most of the cases setting `props.intialState.rowCount=-1` should be enough, but if you want to update the `rowCount` later, you can set the `rowCount` prop or use `apiRef.current.setRowCount(-1)` to set the `rowCount` to `-1` after the Grid is initialized) +- Pass the `paginationMeta.hasNextPage` to let the Grid check on-demand whether more records are available on the server, the Grid will keep fetching the next page until `hasNextPage` is `false` +- Meanwhile, at some point if the BE provides the actual row count, you can update the `rowCount` prop to the actual value to let the Grid know the actual row count + +The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Update Row Count" button. + +{{"demo": "ServerPaginationGridTruncated.js", "bg": "inline"}} + ### Cursor implementation You can also handle servers with cursor-based pagination. diff --git a/packages/x-data-grid-generator/src/hooks/useQuery.ts b/packages/x-data-grid-generator/src/hooks/useQuery.ts index c888d14faa0e..ff4dc2ffc437 100644 --- a/packages/x-data-grid-generator/src/hooks/useQuery.ts +++ b/packages/x-data-grid-generator/src/hooks/useQuery.ts @@ -146,9 +146,11 @@ export const loadServerRows = ( firstRowIndex = page * pageSize; lastRowIndex = (page + 1) * pageSize; } + const hasNextPage = lastRowIndex < filteredRows.length; const response: FakeServerResponse = { returnedRows: filteredRows.slice(firstRowIndex, lastRowIndex), nextCursor, + hasNextPage, totalRowCount, }; @@ -162,12 +164,14 @@ export const loadServerRows = ( interface FakeServerResponse { returnedRows: GridRowModel[]; nextCursor?: string; + hasNextPage: boolean; totalRowCount: number; } interface PageInfo { totalRowCount?: number; nextCursor?: string; + hasNextPage?: boolean; pageSize?: number; } @@ -249,7 +253,7 @@ export const createFakeServer = ( ); (async function fetchData() { - const { returnedRows, nextCursor, totalRowCount } = await loadServerRows( + const { returnedRows, nextCursor, totalRowCount, hasNextPage } = await loadServerRows( rows, queryOptions, serverOptionsWithDefault, @@ -263,6 +267,7 @@ export const createFakeServer = ( pageInfo: { totalRowCount, nextCursor, + hasNextPage, pageSize: returnedRows.length, }, }; diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index ad6815061d5c..894c1346a4b2 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -318,6 +318,12 @@ DataGridPremiumRaw.propTypes = { * @default "cell" */ editMode: PropTypes.oneOf(['cell', 'row']), + /** + * Use if the actual rowCount is not known upfront, but an estimation is available. + * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Applicable only with the server-side data and when `rowCount="-1"` + */ + estimatedRowCount: PropTypes.number, /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, then the feature is fully disabled, and neither property nor method calls will have any effect. @@ -708,6 +714,11 @@ DataGridPremiumRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onMenuOpen: PropTypes.func, + /** + * Callback fired when the pagination meta has changed. + * @param {GridPaginationMeta} paginationMeta Updated pagination meta. + */ + onPaginationMetaChange: PropTypes.func, /** * Callback fired when the pagination model has changed. * @param {GridPaginationModel} model Updated pagination model. @@ -842,6 +853,13 @@ DataGridPremiumRaw.propTypes = { * @default false */ pagination: PropTypes.bool, + /** + * The extra information about the pagination state of the Data Grid. + * Only applicable with `paginationMode="server"`. + */ + paginationMeta: PropTypes.shape({ + hasNextPage: PropTypes.bool, + }), /** * Pagination can be processed on the server or client-side. * Set it to 'client' if you would like to handle the pagination on the client-side. @@ -883,6 +901,7 @@ DataGridPremiumRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Works only with the server-side data. Ignored when `paginationMode === 'client'` */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 55bae2609179..45d35acd1119 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -266,6 +266,12 @@ DataGridProRaw.propTypes = { * @default "cell" */ editMode: PropTypes.oneOf(['cell', 'row']), + /** + * Use if the actual rowCount is not known upfront, but an estimation is available. + * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Applicable only with the server-side data and when `rowCount="-1"` + */ + estimatedRowCount: PropTypes.number, /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, the feature will be fully disabled and any property / method call will not have any effect. @@ -624,6 +630,11 @@ DataGridProRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onMenuOpen: PropTypes.func, + /** + * Callback fired when the pagination meta has changed. + * @param {GridPaginationMeta} paginationMeta Updated pagination meta. + */ + onPaginationMetaChange: PropTypes.func, /** * Callback fired when the pagination model has changed. * @param {GridPaginationModel} model Updated pagination model. @@ -752,6 +763,13 @@ DataGridProRaw.propTypes = { * @default false */ pagination: PropTypes.bool, + /** + * The extra information about the pagination state of the Data Grid. + * Only applicable with `paginationMode="server"`. + */ + paginationMeta: PropTypes.shape({ + hasNextPage: PropTypes.bool, + }), /** * Pagination can be processed on the server or client-side. * Set it to 'client' if you would like to handle the pagination on the client-side. @@ -793,6 +811,7 @@ DataGridProRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Works only with the server-side data. Ignored when `paginationMode === 'client'` */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/x-data-grid/src/DataGrid/DataGrid.tsx index 475dc646e981..a1afd2df5eb6 100644 --- a/packages/x-data-grid/src/DataGrid/DataGrid.tsx +++ b/packages/x-data-grid/src/DataGrid/DataGrid.tsx @@ -225,6 +225,12 @@ DataGridRaw.propTypes = { * @default "cell" */ editMode: PropTypes.oneOf(['cell', 'row']), + /** + * Use if the actual rowCount is not known upfront, but an estimation is available. + * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Applicable only with the server-side data and when `rowCount="-1"` + */ + estimatedRowCount: PropTypes.number, /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, the feature will be fully disabled and any property / method call will not have any effect. @@ -526,6 +532,11 @@ DataGridRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onMenuOpen: PropTypes.func, + /** + * Callback fired when the pagination meta has changed. + * @param {GridPaginationMeta} paginationMeta Updated pagination meta. + */ + onPaginationMetaChange: PropTypes.func, /** * Callback fired when the pagination model has changed. * @param {GridPaginationModel} model Updated pagination model. @@ -630,6 +641,13 @@ DataGridRaw.propTypes = { ]).isRequired, ), pagination: PropTypes.oneOf([true]), + /** + * The extra information about the pagination state of the Data Grid. + * Only applicable with `paginationMode="server"`. + */ + paginationMeta: PropTypes.shape({ + hasNextPage: PropTypes.bool, + }), /** * Pagination can be processed on the server or client-side. * Set it to 'client' if you would like to handle the pagination on the client-side. @@ -660,6 +678,7 @@ DataGridRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Works only with the server-side data. Ignored when `paginationMode === 'client'` */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid/src/components/GridPagination.tsx b/packages/x-data-grid/src/components/GridPagination.tsx index 03be488965ca..e44abff5b713 100644 --- a/packages/x-data-grid/src/components/GridPagination.tsx +++ b/packages/x-data-grid/src/components/GridPagination.tsx @@ -1,15 +1,17 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import TablePagination, { tablePaginationClasses, TablePaginationProps, } from '@mui/material/TablePagination'; -import { styled } from '@mui/material/styles'; +// import TablePagination, { tablePaginationClasses, TablePaginationProps } from './tablePagination'; import { useGridSelector } from '../hooks/utils/useGridSelector'; import { useGridApiContext } from '../hooks/utils/useGridApiContext'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import { gridPaginationModelSelector, gridPaginationRowCountSelector, + gridPageCountSelector, } from '../hooks/features/pagination/gridPaginationSelector'; const GridPaginationRoot = styled(TablePagination)(({ theme }) => ({ @@ -27,6 +29,17 @@ const GridPaginationRoot = styled(TablePagination)(({ theme }) => ({ }, })) as typeof TablePagination; +const getLabelDisplayedRows: ( + estimatedRowCount?: number, +) => TablePaginationProps['labelDisplayedRows'] = + (estimatedRowCount?: number) => + ({ from, to, count }: { from: number; to: number; count: number }) => { + if (!estimatedRowCount) { + return `${from}–${to} of ${count !== -1 ? count : `more than ${to}`}`; + } + return `${from}–${to} of ${count !== -1 ? count : `more than ${estimatedRowCount > to ? estimatedRowCount : to}`}`; + }; + // A mutable version of a readonly array. type MutableArray = A extends readonly (infer T)[] ? T[] : never; @@ -37,11 +50,34 @@ export const GridPagination = React.forwardRef = React.useMemo(() => { + if (paginationMode === 'server' && loading) { + return { + backIconButtonProps: { disabled: true }, + nextIconButtonProps: { disabled: true }, + }; + } + + return {}; + }, [loading, paginationMode]); + + const lastPage = React.useMemo(() => Math.max(0, pageCount - 1), [pageCount]); - const lastPage = React.useMemo(() => { - const calculatedValue = Math.ceil(rowCount / (paginationModel.pageSize || 1)) - 1; - return Math.max(0, calculatedValue); - }, [rowCount, paginationModel.pageSize]); + const computedPage = React.useMemo(() => { + if (rowCount === -1) { + return paginationModel.page; + } + return paginationModel.page <= lastPage ? paginationModel.page : lastPage; + }, [lastPage, paginationModel.page, rowCount]); + + const labelDisplayedRows = React.useMemo( + () => getLabelDisplayedRows(estimatedRowCount), + [estimatedRowCount], + ); const handlePageSizeChange = React.useCallback( (event: React.ChangeEvent) => { @@ -102,7 +138,8 @@ export const GridPagination = React.forwardRef ); }, diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationInterfaces.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationInterfaces.ts index 310c659870fa..a47c14ea4454 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationInterfaces.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationInterfaces.ts @@ -1,13 +1,15 @@ -import { GridPaginationModel } from '../../../models/gridPaginationProps'; +import { GridPaginationMeta, GridPaginationModel } from '../../../models/gridPaginationProps'; export interface GridPaginationState { paginationModel: GridPaginationModel; rowCount: number; + meta: GridPaginationMeta; } export interface GridPaginationInitialState { paginationModel?: Partial; rowCount?: number; + meta?: GridPaginationMeta; } /** @@ -42,7 +44,21 @@ export interface GridPaginationRowCountApi { setRowCount: (rowCount: number) => void; } +/** + * The pagination meta API interface that is available in the grid `apiRef`. + */ +export interface GridPaginationMetaApi { + /** + * Sets the `paginationMeta` to a new value. + * @param {GridPaginationMeta} paginationMeta The new pagination meta value. + */ + setPaginationMeta: (paginationMeta: GridPaginationMeta) => void; +} + /** * The pagination API interface that is available in the grid `apiRef`. */ -export interface GridPaginationApi extends GridPaginationModelApi, GridPaginationRowCountApi {} +export interface GridPaginationApi + extends GridPaginationModelApi, + GridPaginationRowCountApi, + GridPaginationMetaApi {} diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationSelector.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationSelector.ts index bc0b8ef20d2e..786ba8881b45 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationSelector.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationSelector.ts @@ -32,6 +32,15 @@ export const gridPaginationRowCountSelector = createSelector( (pagination) => pagination.rowCount, ); +/** + * Get the pagination meta + * @category Pagination + */ +export const gridPaginationMetaSelector = createSelector( + gridPaginationSelector, + (pagination) => pagination.meta, +); + /** * Get the index of the page to render if the pagination is enabled * @category Pagination @@ -55,9 +64,10 @@ export const gridPageSizeSelector = createSelector( * @category Pagination */ export const gridPageCountSelector = createSelector( - gridPageSizeSelector, + gridPaginationModelSelector, gridPaginationRowCountSelector, - (pageSize, rowCount) => getPageCount(rowCount, pageSize), + (paginationModel, rowCount) => + getPageCount(rowCount, paginationModel.pageSize, paginationModel.page), ); /** diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts index c68819a261ac..601822730c88 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts @@ -6,11 +6,15 @@ const MAX_PAGE_SIZE = 100; export const defaultPageSize = (autoPageSize: boolean) => (autoPageSize ? 0 : 100); -export const getPageCount = (rowCount: number, pageSize: number): number => { +export const getPageCount = (rowCount: number, pageSize: number, page: number): number => { if (pageSize > 0 && rowCount > 0) { return Math.ceil(rowCount / pageSize); } + if (rowCount === -1) { + return page + 1; + } + return 0; }; diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridPagination.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridPagination.ts index 9abbeab5301c..50524ac4e656 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridPagination.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridPagination.ts @@ -9,11 +9,17 @@ import { } from './gridPaginationUtils'; import { useGridPaginationModel } from './useGridPaginationModel'; import { useGridRowCount } from './useGridRowCount'; +import { useGridPaginationMeta } from './useGridPaginationMeta'; export const paginationStateInitializer: GridStateInitializer< Pick< DataGridProcessedProps, - 'paginationModel' | 'rowCount' | 'initialState' | 'autoPageSize' | 'signature' + | 'paginationModel' + | 'rowCount' + | 'initialState' + | 'autoPageSize' + | 'signature' + | 'paginationMeta' > > = (state, props) => { const paginationModel = { @@ -24,11 +30,13 @@ export const paginationStateInitializer: GridStateInitializer< throwIfPageSizeExceedsTheLimit(paginationModel.pageSize, props.signature); const rowCount = props.rowCount ?? props.initialState?.pagination?.rowCount; + const meta = props.paginationMeta ?? props.initialState?.pagination?.meta ?? {}; return { ...state, pagination: { paginationModel, rowCount, + meta, }, }; }; @@ -41,6 +49,7 @@ export const useGridPagination = ( apiRef: React.MutableRefObject, props: DataGridProcessedProps, ) => { + useGridPaginationMeta(apiRef, props); useGridPaginationModel(apiRef, props); useGridRowCount(apiRef, props); }; diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationMeta.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationMeta.ts new file mode 100644 index 000000000000..c87aa8ea6322 --- /dev/null +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationMeta.ts @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; +import { GridPaginationMetaApi } from './gridPaginationInterfaces'; +import { useGridLogger, useGridSelector, useGridApiMethod } from '../../utils'; +import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; +import { gridPaginationMetaSelector } from './gridPaginationSelector'; + +export const useGridPaginationMeta = ( + apiRef: React.MutableRefObject, + props: Pick< + DataGridProcessedProps, + 'paginationMeta' | 'initialState' | 'paginationMode' | 'onPaginationMetaChange' + >, +) => { + const logger = useGridLogger(apiRef, 'useGridPaginationMeta'); + + const paginationMeta = useGridSelector(apiRef, gridPaginationMetaSelector); + + apiRef.current.registerControlState({ + stateId: 'paginationMeta', + propModel: props.paginationMeta, + propOnChange: props.onPaginationMetaChange, + stateSelector: gridPaginationMetaSelector, + changeEvent: 'paginationMetaChange', + }); + + /** + * API METHODS + */ + const setPaginationMeta = React.useCallback( + (newPaginationMeta) => { + if (paginationMeta === newPaginationMeta) { + return; + } + logger.debug("Setting 'paginationMeta' to", newPaginationMeta); + + apiRef.current.setState((state) => ({ + ...state, + pagination: { + ...state.pagination, + meta: newPaginationMeta, + }, + })); + }, + [apiRef, logger, paginationMeta], + ); + + const paginationMetaApi: GridPaginationMetaApi = { + setPaginationMeta, + }; + + useGridApiMethod(apiRef, paginationMetaApi, 'public'); + + /** + * PRE-PROCESSING + */ + const stateExportPreProcessing = React.useCallback>( + (prevState, context) => { + const exportedPaginationMeta = gridPaginationMetaSelector(apiRef); + + const shouldExportRowCount = + // Always export if the `exportOnlyDirtyModels` property is not activated + !context.exportOnlyDirtyModels || + // Always export if the `paginationMeta` is controlled + props.paginationMeta != null || + // Always export if the `paginationMeta` has been initialized + props.initialState?.pagination?.meta != null; + + if (!shouldExportRowCount) { + return prevState; + } + + return { + ...prevState, + pagination: { + ...prevState.pagination, + meta: exportedPaginationMeta, + }, + }; + }, + [apiRef, props.paginationMeta, props.initialState?.pagination?.meta], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + const restoredPaginationMeta = context.stateToRestore.pagination?.meta + ? context.stateToRestore.pagination.meta + : gridPaginationMetaSelector(apiRef); + apiRef.current.setState((state) => ({ + ...state, + pagination: { + ...state.pagination, + meta: restoredPaginationMeta, + }, + })); + return params; + }, + [apiRef], + ); + + useGridRegisterPipeProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPipeProcessor(apiRef, 'restoreState', stateRestorePreProcessing); + + /** + * EFFECTS + */ + React.useEffect(() => { + if (props.paginationMeta) { + apiRef.current.setPaginationMeta(props.paginationMeta); + } + }, [apiRef, props.paginationMeta]); +}; diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts index 579eacf8cfd8..19915042cc5a 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts @@ -12,7 +12,11 @@ import { useGridApiEventHandler, } from '../../utils'; import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; -import { gridPageCountSelector, gridPaginationModelSelector } from './gridPaginationSelector'; +import { + gridPageCountSelector, + gridPaginationModelSelector, + gridPaginationMetaSelector, +} from './gridPaginationSelector'; import { getPageCount, defaultPageSize, @@ -29,7 +33,8 @@ export const getDerivedPaginationModel = ( let paginationModel = paginationState.paginationModel; const rowCount = paginationState.rowCount; const pageSize = paginationModelProp?.pageSize ?? paginationModel.pageSize; - const pageCount = getPageCount(rowCount, pageSize); + const page = paginationModelProp?.page ?? paginationModel.page; + const pageCount = getPageCount(rowCount, pageSize, page); if ( paginationModelProp && diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts index 0e8e76533dbc..50dddcd5494c 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts @@ -1,17 +1,24 @@ import * as React from 'react'; +import useLazyRef from '@mui/utils/useLazyRef'; +import { isNumber } from '../../../utils/utils'; import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { GridPaginationRowCountApi } from './gridPaginationInterfaces'; +import { GridPaginationRowCountApi, GridPaginationState } from './gridPaginationInterfaces'; import { gridFilteredTopLevelRowCountSelector } from '../filter'; -import { useGridLogger, useGridSelector, useGridApiMethod } from '../../utils'; +import { + useGridLogger, + useGridSelector, + useGridApiMethod, + useGridApiEventHandler, +} from '../../utils'; import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; -import { gridPaginationRowCountSelector } from './gridPaginationSelector'; +import { + gridPaginationRowCountSelector, + gridPaginationMetaSelector, + gridPaginationModelSelector, +} from './gridPaginationSelector'; import { noRowCountInServerMode } from './gridPaginationUtils'; -/** - * @requires useGridFilter (state) - * @requires useGridDimensions (event) - can be after - */ export const useGridRowCount = ( apiRef: React.MutableRefObject, props: Pick< @@ -22,7 +29,10 @@ export const useGridRowCount = ( const logger = useGridLogger(apiRef, 'useGridRowCount'); const visibleTopLevelRowCount = useGridSelector(apiRef, gridFilteredTopLevelRowCountSelector); - const rowCount = useGridSelector(apiRef, gridPaginationRowCountSelector); + const rowCountState = useGridSelector(apiRef, gridPaginationRowCountSelector); + const paginationMeta = useGridSelector(apiRef, gridPaginationMetaSelector); + const paginationModel = useGridSelector(apiRef, gridPaginationModelSelector); + const previousPageSize = useLazyRef(() => gridPaginationModelSelector(apiRef).pageSize); apiRef.current.registerControlState({ stateId: 'paginationRowCount', @@ -37,7 +47,7 @@ export const useGridRowCount = ( */ const setRowCount = React.useCallback( (newRowCount) => { - if (rowCount === newRowCount) { + if (rowCountState === newRowCount) { return; } logger.debug("Setting 'rowCount' to", newRowCount); @@ -50,7 +60,7 @@ export const useGridRowCount = ( }, })); }, - [apiRef, logger, rowCount], + [apiRef, logger, rowCountState], ); const paginationRowCountApi: GridPaginationRowCountApi = { @@ -109,6 +119,54 @@ export const useGridRowCount = ( useGridRegisterPipeProcessor(apiRef, 'exportState', stateExportPreProcessing); useGridRegisterPipeProcessor(apiRef, 'restoreState', stateRestorePreProcessing); + /** + * EVENTS + */ + const handlePaginationMetaChange = React.useCallback( + (meta: GridPaginationState['meta']) => { + if (props.paginationMode === 'client') { + return; + } + if (meta.hasNextPage === false && rowCountState === -1) { + apiRef.current.setRowCount(paginationModel.pageSize * (paginationModel.page + 1)); + return; + } + + if (meta.hasNextPage === true && rowCountState !== -1) { + const lastPage = Math.max(0, Math.ceil(rowCountState / paginationModel.pageSize) - 1); + if (paginationModel.page === lastPage) { + apiRef.current.setRowCount(-1); + } + } + }, + [apiRef, props.paginationMode, rowCountState, paginationModel], + ); + + const handlePaginationModelChange = React.useCallback( + (model: GridPaginationState['paginationModel']) => { + if (props.paginationMode === 'client' || !previousPageSize.current) { + return; + } + if (model.pageSize !== previousPageSize.current) { + // The page size has changed + previousPageSize.current = model.pageSize; + if (rowCountState !== -1) { + // Row count unknown and page size changed, reset the page + apiRef.current.setPage(0); + return; + } + const lastPage = Math.max(0, Math.ceil(rowCountState / model.pageSize) - 1); + if (model.page > lastPage) { + apiRef.current.setPage(lastPage); + } + } + }, + [props.paginationMode, previousPageSize, rowCountState, apiRef], + ); + + useGridApiEventHandler(apiRef, 'paginationModelChange', handlePaginationModelChange); + useGridApiEventHandler(apiRef, 'paginationMetaChange', handlePaginationMetaChange); + /** * EFFECTS */ @@ -123,8 +181,18 @@ export const useGridRowCount = ( React.useEffect(() => { if (props.paginationMode === 'client') { apiRef.current.setRowCount(visibleTopLevelRowCount); - } else if (props.rowCount != null) { + return; + } + + if (isNumber(props.rowCount)) { apiRef.current.setRowCount(props.rowCount); } - }, [apiRef, visibleTopLevelRowCount, props.paginationMode, props.rowCount]); + }, [ + apiRef, + visibleTopLevelRowCount, + props.paginationMode, + props.rowCount, + paginationMeta, + paginationModel, + ]); }; diff --git a/packages/x-data-grid/src/internals/utils/propValidation.ts b/packages/x-data-grid/src/internals/utils/propValidation.ts index b86261d3ca8c..e2902b0e9520 100644 --- a/packages/x-data-grid/src/internals/utils/propValidation.ts +++ b/packages/x-data-grid/src/internals/utils/propValidation.ts @@ -13,6 +13,14 @@ export const propValidatorsDataGrid: PropValidator[] = [ 'Please remove one of these two props.', ].join('\n')) || undefined, + (props) => + (props.paginationMode === 'client' && + props.paginationMeta != null && + [ + 'MUI X: `paginationMeta` is not used when `paginationMode` is `client`.', + 'Consider removing the `paginationMeta` prop or switching to `server` mode if your pagination is on server.', + ].join('\n')) || + undefined, ]; const warnedOnceMap = new Set(); diff --git a/packages/x-data-grid/src/models/events/gridEventLookup.ts b/packages/x-data-grid/src/models/events/gridEventLookup.ts index 451988f596a1..20581f4c474a 100644 --- a/packages/x-data-grid/src/models/events/gridEventLookup.ts +++ b/packages/x-data-grid/src/models/events/gridEventLookup.ts @@ -24,7 +24,7 @@ import type { GridColumnVisibilityModel } from '../../hooks/features/columns'; import type { GridStrategyProcessorName } from '../../hooks/core/strategyProcessing'; import { GridRowEditStartParams, GridRowEditStopParams } from '../params/gridRowParams'; import { GridCellModesModel, GridRowModesModel } from '../api/gridEditingApi'; -import { GridPaginationModel } from '../gridPaginationProps'; +import { GridPaginationMeta, GridPaginationModel } from '../gridPaginationProps'; export interface GridRowEventLookup { /** @@ -362,6 +362,10 @@ export interface GridControlledStateEventLookup { * Fired when the row count change. */ rowCountChange: { params: number }; + /** + * Fired when the pagination meta change. + */ + paginationMetaChange: { params: GridPaginationMeta }; } export interface GridControlledStateReasonLookup { diff --git a/packages/x-data-grid/src/models/gridPaginationProps.ts b/packages/x-data-grid/src/models/gridPaginationProps.ts index 543323ac86b1..0e9395f39a9f 100644 --- a/packages/x-data-grid/src/models/gridPaginationProps.ts +++ b/packages/x-data-grid/src/models/gridPaginationProps.ts @@ -11,3 +11,7 @@ export interface GridPaginationModel { */ page: number; } + +export interface GridPaginationMeta { + hasNextPage?: boolean; +} diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 90c2b031ccbe..9fa8875fae6a 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -30,7 +30,7 @@ import { GridSlotsComponentsProps } from '../gridSlotsComponentsProps'; import { GridColumnVisibilityModel } from '../../hooks/features/columns/gridColumnsInterfaces'; import { GridCellModesModel, GridRowModesModel } from '../api/gridEditingApi'; import { GridColumnGroupingModel } from '../gridColumnGrouping'; -import { GridPaginationModel } from '../gridPaginationProps'; +import { GridPaginationMeta, GridPaginationModel } from '../gridPaginationProps'; import type { GridAutosizeOptions } from '../../hooks/features/columnResize'; export interface GridExperimentalFeatures { @@ -411,8 +411,15 @@ export interface DataGridPropsWithoutDefaultValue void; + /** + * Callback fired when the pagination meta has changed. + * @param {GridPaginationMeta} paginationMeta Updated pagination meta. + */ + onPaginationMetaChange?: (paginationMeta: GridPaginationMeta) => void; /** * Callback fired when the preferences panel is closed. * @param {GridPreferencePanelParams} params With all properties from [[GridPreferencePanelParams]]. From 09ec00cc6e141885840c9424b5590b6067bcf045 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 18 Mar 2024 13:17:21 +0500 Subject: [PATCH 02/27] Wrap labelDisplayedRows --- .../pagination/CursorPaginationGrid.tsx | 4 -- .../src/components/GridPagination.tsx | 46 +++++++++++-------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx index 716e2782f38a..d54e3e806c7d 100644 --- a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx +++ b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx @@ -68,10 +68,6 @@ export default function CursorPaginationGrid() { return paginationMetaRef.current; }, [hasNextPage]); - React.useEffect(() => { - console.log(paginationMeta); - }, [paginationMeta]); - React.useEffect(() => { if (!isLoading && nextCursor) { // We add nextCursor when available diff --git a/packages/x-data-grid/src/components/GridPagination.tsx b/packages/x-data-grid/src/components/GridPagination.tsx index e44abff5b713..fb5c291c2689 100644 --- a/packages/x-data-grid/src/components/GridPagination.tsx +++ b/packages/x-data-grid/src/components/GridPagination.tsx @@ -3,8 +3,8 @@ import { styled } from '@mui/material/styles'; import TablePagination, { tablePaginationClasses, TablePaginationProps, + LabelDisplayedRowsArgs, } from '@mui/material/TablePagination'; -// import TablePagination, { tablePaginationClasses, TablePaginationProps } from './tablePagination'; import { useGridSelector } from '../hooks/utils/useGridSelector'; import { useGridApiContext } from '../hooks/utils/useGridApiContext'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; @@ -29,16 +29,24 @@ const GridPaginationRoot = styled(TablePagination)(({ theme }) => ({ }, })) as typeof TablePagination; -const getLabelDisplayedRows: ( - estimatedRowCount?: number, -) => TablePaginationProps['labelDisplayedRows'] = - (estimatedRowCount?: number) => - ({ from, to, count }: { from: number; to: number; count: number }) => { - if (!estimatedRowCount) { - return `${from}–${to} of ${count !== -1 ? count : `more than ${to}`}`; - } - return `${from}–${to} of ${count !== -1 ? count : `more than ${estimatedRowCount > to ? estimatedRowCount : to}`}`; - }; +type WrappedLabelDisplayedRows = ( + args: LabelDisplayedRowsArgs & { estimated?: number }, +) => React.ReactNode; + +const wrapLabelDisplayedRows = ( + labelDisplayedRows: WrappedLabelDisplayedRows, + estimated?: number, +): TablePaginationProps['labelDisplayedRows'] => { + return ({ from, to, count, page }: LabelDisplayedRowsArgs) => + labelDisplayedRows!({ from, to, count, page, estimated }); +}; + +const defaultLabelDisplayedRows: WrappedLabelDisplayedRows = ({ from, to, count, estimated }) => { + if (!estimated) { + return `${from}–${to} of ${count !== -1 ? count : `more than ${to}`}`; + } + return `${from}–${to} of ${count !== -1 ? count : `more than ${estimated > to ? estimated : to}`}`; +}; // A mutable version of a readonly array. @@ -74,11 +82,6 @@ export const GridPagination = React.forwardRef getLabelDisplayedRows(estimatedRowCount), - [estimatedRowCount], - ); - const handlePageSizeChange = React.useCallback( (event: React.ChangeEvent) => { const pageSize = Number(event.target.value); @@ -133,12 +136,17 @@ export const GridPagination = React.forwardRef ); }, From 2bdba17313fcaf4da7063fc5426e924dce280c02 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 18 Mar 2024 13:17:58 +0500 Subject: [PATCH 03/27] Rebuild docs --- docs/data/data-grid/pagination/CursorPaginationGrid.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.js b/docs/data/data-grid/pagination/CursorPaginationGrid.js index 629081742a55..04caad836070 100644 --- a/docs/data/data-grid/pagination/CursorPaginationGrid.js +++ b/docs/data/data-grid/pagination/CursorPaginationGrid.js @@ -61,10 +61,6 @@ export default function CursorPaginationGrid() { return paginationMetaRef.current; }, [hasNextPage]); - React.useEffect(() => { - console.log(paginationMeta); - }, [paginationMeta]); - React.useEffect(() => { if (!isLoading && nextCursor) { // We add nextCursor when available From 6df8b1518dec33df0acdc80b7a221faae7cfc942 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 18 Mar 2024 14:25:43 +0500 Subject: [PATCH 04/27] Housekeeping --- docs/data/data-grid/events/events.json | 7 +++++++ docs/pages/x/api/data-grid/data-grid-premium.json | 9 +++++++++ docs/pages/x/api/data-grid/data-grid-pro.json | 9 +++++++++ docs/pages/x/api/data-grid/data-grid.json | 9 +++++++++ docs/pages/x/api/data-grid/grid-api.md | 1 + docs/pages/x/api/data-grid/grid-pagination-api.json | 5 +++++ docs/pages/x/api/data-grid/selectors.json | 7 +++++++ .../data-grid-premium/data-grid-premium.json | 12 +++++++++++- .../data-grid/data-grid-pro/data-grid-pro.json | 12 +++++++++++- .../api-docs/data-grid/data-grid/data-grid.json | 12 +++++++++++- .../features/pagination/useGridPaginationModel.ts | 6 +----- scripts/x-data-grid-premium.exports.json | 2 ++ scripts/x-data-grid-pro.exports.json | 2 ++ scripts/x-data-grid.exports.json | 2 ++ 14 files changed, 87 insertions(+), 8 deletions(-) diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json index 6bb6029718fa..af85b176b40e 100644 --- a/docs/data/data-grid/events/events.json +++ b/docs/data/data-grid/events/events.json @@ -247,6 +247,13 @@ "event": "MuiEvent<{}>", "componentProp": "onMenuOpen" }, + { + "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"], + "name": "paginationMetaChange", + "description": "Fired when the pagination meta change.", + "params": "GridPaginationMeta", + "event": "MuiEvent<{}>" + }, { "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"], "name": "paginationModelChange", 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 ba181402628c..3a6ff9b2480e 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -78,6 +78,7 @@ "type": { "name": "enum", "description": "'cell'
| 'row'" }, "default": "\"cell\"" }, + "estimatedRowCount": { "type": { "name": "number" } }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ warnIfFocusStateIsNotSynced?: bool }" } }, @@ -399,6 +400,13 @@ "describedArgs": ["params", "event", "details"] } }, + "onPaginationMetaChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(paginationMeta: GridPaginationMeta) => void", + "describedArgs": ["paginationMeta"] + } + }, "onPaginationModelChange": { "type": { "name": "func" }, "signature": { @@ -520,6 +528,7 @@ "default": "[25, 50, 100]" }, "pagination": { "type": { "name": "bool" }, "default": "false" }, + "paginationMeta": { "type": { "name": "shape", "description": "{ hasNextPage?: bool }" } }, "paginationMode": { "type": { "name": "enum", "description": "'client'
| 'server'" }, "default": "\"client\"" 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 198c5f68c6da..6ff8f672c4d7 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -64,6 +64,7 @@ "type": { "name": "enum", "description": "'cell'
| 'row'" }, "default": "\"cell\"" }, + "estimatedRowCount": { "type": { "name": "number" } }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ warnIfFocusStateIsNotSynced?: bool }" } }, @@ -353,6 +354,13 @@ "describedArgs": ["params", "event", "details"] } }, + "onPaginationMetaChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(paginationMeta: GridPaginationMeta) => void", + "describedArgs": ["paginationMeta"] + } + }, "onPaginationModelChange": { "type": { "name": "func" }, "signature": { @@ -467,6 +475,7 @@ "default": "[25, 50, 100]" }, "pagination": { "type": { "name": "bool" }, "default": "false" }, + "paginationMeta": { "type": { "name": "shape", "description": "{ hasNextPage?: bool }" } }, "paginationMode": { "type": { "name": "enum", "description": "'client'
| 'server'" }, "default": "\"client\"" diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 468423143dd7..99deda9e3618 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -53,6 +53,7 @@ "type": { "name": "enum", "description": "'cell'
| 'row'" }, "default": "\"cell\"" }, + "estimatedRowCount": { "type": { "name": "number" } }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ warnIfFocusStateIsNotSynced?: bool }" } }, @@ -299,6 +300,13 @@ "describedArgs": ["params", "event", "details"] } }, + "onPaginationMetaChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(paginationMeta: GridPaginationMeta) => void", + "describedArgs": ["paginationMeta"] + } + }, "onPaginationModelChange": { "type": { "name": "func" }, "signature": { @@ -391,6 +399,7 @@ }, "default": "[25, 50, 100]" }, + "paginationMeta": { "type": { "name": "shape", "description": "{ hasNextPage?: bool }" } }, "paginationMode": { "type": { "name": "enum", "description": "'client'
| 'server'" }, "default": "\"client\"" diff --git a/docs/pages/x/api/data-grid/grid-api.md b/docs/pages/x/api/data-grid/grid-api.md index cf6a2bdbf7de..2fb3ca1dfe7f 100644 --- a/docs/pages/x/api/data-grid/grid-api.md +++ b/docs/pages/x/api/data-grid/grid-api.md @@ -112,6 +112,7 @@ import { GridApi } from '@mui/x-data-grid'; | setFilterModel | (model: GridFilterModel, reason?: GridControlledStateReasonLookup['filter']) => void | Sets the filter model to the one given by `model`. | | setPage | (page: number) => void | Sets the displayed page to the value given by `page`. | | setPageSize | (pageSize: number) => void | Sets the number of displayed rows to the value given by `pageSize`. | +| setPaginationMeta | (paginationMeta: GridPaginationMeta) => void | Sets the `paginationMeta` to a new value. | | setPaginationModel | (model: GridPaginationModel) => void | Sets the `paginationModel` to a new value. | | setPinnedColumns [](/x/introduction/licensing/#pro-plan) | (pinnedColumns: GridPinnedColumnFields) => void | Changes the pinned columns. | | setQuickFilterValues | (values: any[]) => void | Set the quick filter values to the one given by `values` | diff --git a/docs/pages/x/api/data-grid/grid-pagination-api.json b/docs/pages/x/api/data-grid/grid-pagination-api.json index b782ce762515..47f7684b511e 100644 --- a/docs/pages/x/api/data-grid/grid-pagination-api.json +++ b/docs/pages/x/api/data-grid/grid-pagination-api.json @@ -12,6 +12,11 @@ "description": "Sets the number of displayed rows to the value given by pageSize.", "type": "(pageSize: number) => void" }, + { + "name": "setPaginationMeta", + "description": "Sets the paginationMeta to a new value.", + "type": "(paginationMeta: GridPaginationMeta) => void" + }, { "name": "setPaginationModel", "description": "Sets the paginationModel to a new value.", diff --git a/docs/pages/x/api/data-grid/selectors.json b/docs/pages/x/api/data-grid/selectors.json index fc2fa812d481..36c3122925eb 100644 --- a/docs/pages/x/api/data-grid/selectors.json +++ b/docs/pages/x/api/data-grid/selectors.json @@ -317,6 +317,13 @@ "description": "Get the id of each row to include in the current page if the pagination is enabled.", "supportsApiRef": true }, + { + "name": "gridPaginationMetaSelector", + "returnType": "GridPaginationMeta", + "category": "Pagination", + "description": "Get the pagination meta", + "supportsApiRef": true + }, { "name": "gridPaginationModelSelector", "returnType": "GridPaginationModel", 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 7f8c28d5b717..ecea15780291 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 @@ -107,6 +107,9 @@ "description": "If true, the virtualization is disabled." }, "editMode": { "description": "Controls whether to use the cell or row editing." }, + "estimatedRowCount": { + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with the server-side data and when rowCount="-1"" + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, then the feature is fully disabled, and neither property nor method calls will have any effect." }, @@ -432,6 +435,10 @@ "details": "Additional details for this callback." } }, + "onPaginationMetaChange": { + "description": "Callback fired when the pagination meta has changed.", + "typeDescriptions": { "paginationMeta": "Updated pagination meta." } + }, "onPaginationModelChange": { "description": "Callback fired when the pagination model has changed.", "typeDescriptions": { @@ -554,6 +561,9 @@ }, "pageSizeOptions": { "description": "Select the pageSize dynamically using the component UI." }, "pagination": { "description": "If true, pagination is enabled." }, + "paginationMeta": { + "description": "The extra information about the pagination state of the Data Grid. Only applicable with paginationMode="server"." + }, "paginationMode": { "description": "Pagination can be processed on the server or client-side. Set it to 'client' if you would like to handle the pagination on the client-side. Set it to 'server' if you would like to handle the pagination on the server-side." }, @@ -574,7 +584,7 @@ "description": "Number of extra rows to be rendered before/after the visible slice." }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows." + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Works only with the server-side data. Ignored when paginationMode === 'client'" }, "rowGroupingColumnMode": { "description": "If single, all the columns that are grouped are represented in the same grid column. If multiple, each column that is grouped is represented in its own grid column." 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 c4f04eb81c60..9135dbf367fc 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 @@ -95,6 +95,9 @@ "description": "If true, the virtualization is disabled." }, "editMode": { "description": "Controls whether to use the cell or row editing." }, + "estimatedRowCount": { + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with the server-side data and when rowCount="-1"" + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." }, @@ -389,6 +392,10 @@ "details": "Additional details for this callback." } }, + "onPaginationMetaChange": { + "description": "Callback fired when the pagination meta has changed.", + "typeDescriptions": { "paginationMeta": "Updated pagination meta." } + }, "onPaginationModelChange": { "description": "Callback fired when the pagination model has changed.", "typeDescriptions": { @@ -504,6 +511,9 @@ }, "pageSizeOptions": { "description": "Select the pageSize dynamically using the component UI." }, "pagination": { "description": "If true, pagination is enabled." }, + "paginationMeta": { + "description": "The extra information about the pagination state of the Data Grid. Only applicable with paginationMode="server"." + }, "paginationMode": { "description": "Pagination can be processed on the server or client-side. Set it to 'client' if you would like to handle the pagination on the client-side. Set it to 'server' if you would like to handle the pagination on the server-side." }, @@ -524,7 +534,7 @@ "description": "Number of extra rows to be rendered before/after the visible slice." }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows." + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Works only with the server-side data. Ignored when paginationMode === 'client'" }, "rowHeight": { "description": "Sets the height in pixel of a row in the Data Grid." }, "rowModesModel": { "description": "Controls the modes of the rows." }, diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index c6d25a3f35a8..890aa700273a 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -70,6 +70,9 @@ "description": "If true, the virtualization is disabled." }, "editMode": { "description": "Controls whether to use the cell or row editing." }, + "estimatedRowCount": { + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with the server-side data and when rowCount="-1"" + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." }, @@ -318,6 +321,10 @@ "details": "Additional details for this callback." } }, + "onPaginationMetaChange": { + "description": "Callback fired when the pagination meta has changed.", + "typeDescriptions": { "paginationMeta": "Updated pagination meta." } + }, "onPaginationModelChange": { "description": "Callback fired when the pagination model has changed.", "typeDescriptions": { @@ -409,6 +416,9 @@ } }, "pageSizeOptions": { "description": "Select the pageSize dynamically using the component UI." }, + "paginationMeta": { + "description": "The extra information about the pagination state of the Data Grid. Only applicable with paginationMode="server"." + }, "paginationMode": { "description": "Pagination can be processed on the server or client-side. Set it to 'client' if you would like to handle the pagination on the client-side. Set it to 'server' if you would like to handle the pagination on the server-side." }, @@ -427,7 +437,7 @@ "description": "Number of extra rows to be rendered before/after the visible slice." }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows." + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Works only with the server-side data. Ignored when paginationMode === 'client'" }, "rowHeight": { "description": "Sets the height in pixel of a row in the Data Grid." }, "rowModesModel": { "description": "Controls the modes of the rows." }, diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts index 19915042cc5a..c482999e44ac 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts @@ -12,11 +12,7 @@ import { useGridApiEventHandler, } from '../../utils'; import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; -import { - gridPageCountSelector, - gridPaginationModelSelector, - gridPaginationMetaSelector, -} from './gridPaginationSelector'; +import { gridPageCountSelector, gridPaginationModelSelector } from './gridPaginationSelector'; import { getPageCount, defaultPageSize, diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index 227740915c9d..4ca240609f4c 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -408,6 +408,8 @@ { "name": "GridPagination", "kind": "Variable" }, { "name": "GridPaginationApi", "kind": "Interface" }, { "name": "GridPaginationInitialState", "kind": "Interface" }, + { "name": "GridPaginationMeta", "kind": "Interface" }, + { "name": "gridPaginationMetaSelector", "kind": "Variable" }, { "name": "GridPaginationModel", "kind": "Interface" }, { "name": "gridPaginationModelSelector", "kind": "Variable" }, { "name": "gridPaginationRowCountSelector", "kind": "Variable" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index f9e4d9888c0e..b822b777300a 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -372,6 +372,8 @@ { "name": "GridPagination", "kind": "Variable" }, { "name": "GridPaginationApi", "kind": "Interface" }, { "name": "GridPaginationInitialState", "kind": "Interface" }, + { "name": "GridPaginationMeta", "kind": "Interface" }, + { "name": "gridPaginationMetaSelector", "kind": "Variable" }, { "name": "GridPaginationModel", "kind": "Interface" }, { "name": "gridPaginationModelSelector", "kind": "Variable" }, { "name": "gridPaginationRowCountSelector", "kind": "Variable" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index 5464d87a7253..d993afa228e7 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -343,6 +343,8 @@ { "name": "GridPagination", "kind": "Variable" }, { "name": "GridPaginationApi", "kind": "Interface" }, { "name": "GridPaginationInitialState", "kind": "Interface" }, + { "name": "GridPaginationMeta", "kind": "Interface" }, + { "name": "gridPaginationMetaSelector", "kind": "Variable" }, { "name": "GridPaginationModel", "kind": "Interface" }, { "name": "gridPaginationModelSelector", "kind": "Variable" }, { "name": "gridPaginationRowCountSelector", "kind": "Variable" }, From 10423db54cf3b02ad13d7193932b7efa1294af6a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 18 Mar 2024 14:49:17 +0500 Subject: [PATCH 05/27] Fix test --- .../src/tests/statePersistence.DataGridPro.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx index 434c23c56c1c..85137e831e80 100644 --- a/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx @@ -63,6 +63,7 @@ const FULL_INITIAL_STATE: GridInitialState = { }, }, pagination: { + meta: {}, paginationModel: { page: 1, pageSize: 2 }, rowCount: 6, }, @@ -125,6 +126,7 @@ describe(' - State persistence', () => { filterModel: getDefaultGridFilterModel(), }, pagination: { + meta: {}, paginationModel: { page: 0, pageSize: 100 }, rowCount: 6, }, @@ -188,6 +190,7 @@ describe(' - State persistence', () => { pageSize: FULL_INITIAL_STATE.pagination?.paginationModel?.pageSize!, }} rowCount={FULL_INITIAL_STATE.pagination?.rowCount} + paginationMeta={FULL_INITIAL_STATE.pagination?.meta} pinnedColumns={FULL_INITIAL_STATE.pinnedColumns} // Some portable states don't have a controllable model initialState={{ @@ -197,6 +200,7 @@ describe(' - State persistence', () => { }, preferencePanel: FULL_INITIAL_STATE.preferencePanel, }} + paginationMode="server" />, ); expect(apiRef.current.exportState({ exportOnlyDirtyModels: true })).to.deep.equal( From 1e099fef2212cf631382751da5a10c7dea403efc Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 18 Mar 2024 14:55:20 +0500 Subject: [PATCH 06/27] CI From 595d79f6970e4c1b410e3a5db12aaf7c85a95a30 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 21 Mar 2024 11:16:59 +0500 Subject: [PATCH 07/27] Improve docs a bit --- docs/data/data-grid/pagination/pagination.md | 44 +++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index ef26207384ee..597aa6de350f 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -109,21 +109,29 @@ Check out [Selection—Usage with server-side pagination](/x/react-data-grid/row The index-based pagination is the most common type of pagination. It follows the same pattern as the client-side pagination (page, pageSize), but the data is fetched from the server. -It can be further categorized into sub-types based on the availability of the total number of rows or `rowCount`. There can three different possibilities: +It can be further categorized into sub-types based on the availability of the total number of rows or `rowCount`. + +You can provide the `rowCount` using one of the following ways: + +- Initialize - use `initialState.pagination.rowCount` prop to initialize the `rowCount` +- Control - use the `rowCount` prop along with `onRowCountChange` to control the `rowCount` and reflect the change it when the row count is updated +- Set using the `apiRef` - use the `apiRef.current.setRowCount` method to set the `rowCount` after the Grid is initialized + +There can be three different possibilities regarding the availability of the `rowCount` on the server-side: 1. Row count is available 2. Row count is not available 3. Row count is available but is not accurate and may update later on -In the following sections, we will discuss how to handle each of these cases. +The following sections will guide you through the implementation of each of these possibilities. #### 1. Row count is available When the row count is available, you can use the following steps to enable server-side pagination: - Set the prop `paginationMode` to `server` -- Provide the `rowCount` using the `initialState.pagination.rowCount` or the `rowCount` prop to let the data grid calculate how many pages are there -- Use the `onPaginationModelChange` prop callback to load the rows when the page changes +- Provide the `rowCount` value using one of the above mentioned ways +- Use the `onPaginationModelChange` prop to load the rows when the page changes Since the `rowCount` prop is used to compute the number of available pages, switching it to `undefined` during loading resets the page to zero. To avoid this problem, you can keep the previous value of `rowCount` while loading as follows: @@ -146,13 +154,13 @@ React.useEffect(() => { When the row count is not available, you can use the following steps to enable server-side pagination: - Set the prop `paginationMode` to `server` -- Use the `onPaginationModelChange` prop callback to load the rows when the page changes -- Pass the initial `rowCount` using the `initialState.pagination.rowCount` or the `rowCount` prop as `-1` to let the Grid know that the row count is not available -- Since the `rowCount` is unknown, the Grid needs to know whether more records are available on the server. You can use the `paginationMeta.hasNextPage` property to indicate whether more records are available. +- Use the `onPaginationModelChange` prop to load the rows when the page changes +- Set the `rowCount` as `-1` using one of the above mentioned ways to let the Grid know that the row count is not available +- Since the `rowCount` is unknown, the Grid needs to know whether more records are available on the server. Pass the `paginationMeta.hasNextPage` boolean property to indicate whether more records are available. -The `hasNextPage` must not be set to `false` until there are actually no records left to fetch, because when `hasNextPage` becomes false, the Grid sets the `rowCount` to the currently fetched rows to switch back to the known rows case. +The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` to the currently fetched rows. This can lead to the Grid showing the wrong number of pages and rows or cause a flickering. (Alternatively, you can set the `rowCount` to the actual number of rows fetched from the server as soon as the information is available.) -The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook, so you can memoize the `paginationMeta` value to avoid unnecessary flickering of the pagination UI: +The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook, one possible solution could be to memoize the `paginationMeta` value to avoid unnecessary flickering of the pagination UI: ```tsx const paginationMetaRef = React.useRef(); @@ -172,22 +180,26 @@ const paginationMeta = React.useMemo(() => { #### 3. Row count is available but is not accurate -There could be possibilities when the accurate row count is not initially available, either because computing it upfront is a costly operation or it could not be known until the last page is fetched. In such cases, the backend could send an estimated row count which could be initially used to calculate the number of pages. Pass the estimated row count using the `estimatedRowCount` prop and set the `rowCount` to `-1` to indicate that the actual row count is not (yet) available. +There could be possibilities when the accurate row count is not initially available for many reasons such as: -There could be two further possibilities: +1. For some databases, computing `rowCount` upfront is a costly operationdue to scale of data or how the data is structured +2. Some data structures don't have rowCount information until the very last page is fetched. -1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop, either by updating the `rowCount` prop or using the `setRowCount` method of the `apiRef`. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a value >= 0. -2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage` prop to indicate whether more records are available on the server. +In such cases, the backend could send an estimated row count which could be initially used to calculate the number of pages. In some cases, this estimate value could also be more than the actual row count. So the Grid uses it only to show the user an estimated number of rows, but relies on the `hasNextPage` prop to check whether more records are available on the server. Here are the steps to achieve it: - Set the prop `paginationMode` to `server` - Use the `onPaginationModelChange` prop callback to load the rows when the page changes -- Pass the initial estimated `rowCount` using the `estimatedRowCount` prop and set the `rowCount` as `-1` (in most of the cases setting `props.intialState.rowCount=-1` should be enough, but if you want to update the `rowCount` later, you can set the `rowCount` prop or use `apiRef.current.setRowCount(-1)` to set the `rowCount` to `-1` after the Grid is initialized) +- Pass the estimated row count using the `estimatedRowCount` prop and set the `rowCount` to `-1` to indicate that the actual row count is not (yet) available. - Pass the `paginationMeta.hasNextPage` to let the Grid check on-demand whether more records are available on the server, the Grid will keep fetching the next page until `hasNextPage` is `false` -- Meanwhile, at some point if the BE provides the actual row count, you can update the `rowCount` prop to the actual value to let the Grid know the actual row count -The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Update Row Count" button. +There could be two further possibilities: + +1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a value >= 0. +2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage=false` to know that the last page is fetched. + +The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Set Row Count" button. {{"demo": "ServerPaginationGridTruncated.js", "bg": "inline"}} From 07365d93c0e7dcc183ed53ecc44091ae39a2c814 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 21 Mar 2024 12:38:07 +0500 Subject: [PATCH 08/27] Improvement --- .../ServerPaginationGridNoRowCount.js | 15 ++++++++++++-- .../ServerPaginationGridNoRowCount.tsx | 20 +++++++++++++++++-- ...ServerPaginationGridNoRowCount.tsx.preview | 1 + docs/data/data-grid/pagination/pagination.md | 18 ++++++++++------- .../features/pagination/useGridRowCount.ts | 13 ++++-------- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js index b86536250190..56cdf70c5c25 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js @@ -1,14 +1,17 @@ import * as React from 'react'; -import { DataGrid } from '@mui/x-data-grid'; +import { DataGrid, useGridApiRef } from '@mui/x-data-grid'; import { createFakeServer } from '@mui/x-data-grid-generator'; const SERVER_OPTIONS = { useCursorPagination: false, }; -const { useQuery, ...data } = createFakeServer({}, SERVER_OPTIONS); +const rowLength = 98; + +const { useQuery, ...data } = createFakeServer({ rowLength }, SERVER_OPTIONS); export default function ServerPaginationGridNoRowCount() { + const apiRef = useGridApiRef(); const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 5, @@ -37,9 +40,17 @@ export default function ServerPaginationGridNoRowCount() { setPaginationModel(newPaginationModel); }, []); + React.useEffect(() => { + if (paginationMeta?.hasNextPage === false) { + // On last page set the row count to the number of rows + apiRef.current.setRowCount(rowLength); + } + }, [apiRef, paginationMeta]); + return (
{ + if (paginationMeta?.hasNextPage === false) { + // On last page set the row count to the number of rows + apiRef.current.setRowCount(rowLength); + } + }, [apiRef, paginationMeta]); + return (
{ When the row count is not available, you can use the following steps to enable server-side pagination: - Set the prop `paginationMode` to `server` -- Use the `onPaginationModelChange` prop to load the rows when the page changes - Set the `rowCount` as `-1` using one of the above mentioned ways to let the Grid know that the row count is not available - Since the `rowCount` is unknown, the Grid needs to know whether more records are available on the server. Pass the `paginationMeta.hasNextPage` boolean property to indicate whether more records are available. +- Use the `onPaginationModelChange` prop to react to the `paginationModel` changes + +The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value. + +Relying on this behavior of the Grid should be the last resort though, the preferred way is to set the `rowCount` to the actual number of rows fetched from the server as soon as the information is available since the Grid cannot guess if the rows in the last page are less than the page size, and the incorrect number might be displayed in the pagination UI. -The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` to the currently fetched rows. This can lead to the Grid showing the wrong number of pages and rows or cause a flickering. (Alternatively, you can set the `rowCount` to the actual number of rows fetched from the server as soon as the information is available.) +For example the if the row count is `15` and the page size is `10`, on the last page, when `hasNextPage` becomes false the Grid will set the row count to be `page * pageSize` (20) which will show `10-20 of 20` which is incorrect, the correct value should be `10-15 of 15`. The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook, one possible solution could be to memoize the `paginationMeta` value to avoid unnecessary flickering of the pagination UI: @@ -182,22 +186,22 @@ const paginationMeta = React.useMemo(() => { There could be possibilities when the accurate row count is not initially available for many reasons such as: -1. For some databases, computing `rowCount` upfront is a costly operationdue to scale of data or how the data is structured -2. Some data structures don't have rowCount information until the very last page is fetched. +1. For some databases, computing `rowCount` upfront is a costly operation due to the scale of data or how the data is structured. +2. Some data structures don't have the `rowCount` information until the very last page. In such cases, the backend could send an estimated row count which could be initially used to calculate the number of pages. In some cases, this estimate value could also be more than the actual row count. So the Grid uses it only to show the user an estimated number of rows, but relies on the `hasNextPage` prop to check whether more records are available on the server. Here are the steps to achieve it: - Set the prop `paginationMode` to `server` -- Use the `onPaginationModelChange` prop callback to load the rows when the page changes - Pass the estimated row count using the `estimatedRowCount` prop and set the `rowCount` to `-1` to indicate that the actual row count is not (yet) available. - Pass the `paginationMeta.hasNextPage` to let the Grid check on-demand whether more records are available on the server, the Grid will keep fetching the next page until `hasNextPage` is `false` +- Use the `onPaginationModelChange` prop to react to the `paginationModel` changes There could be two further possibilities: -1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a value >= 0. -2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage=false` to know that the last page is fetched. +1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a positive value. +2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage=false` to know that the last page is fetched and try to set a value. Option 1 should be the preferred way to solve this though. The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Set Row Count" button. diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts index 50dddcd5494c..653204c0bf95 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts @@ -127,16 +127,11 @@ export const useGridRowCount = ( if (props.paginationMode === 'client') { return; } - if (meta.hasNextPage === false && rowCountState === -1) { - apiRef.current.setRowCount(paginationModel.pageSize * (paginationModel.page + 1)); - return; - } - if (meta.hasNextPage === true && rowCountState !== -1) { - const lastPage = Math.max(0, Math.ceil(rowCountState / paginationModel.pageSize) - 1); - if (paginationModel.page === lastPage) { - apiRef.current.setRowCount(-1); - } + if (!meta.hasNextPage && rowCountState === -1) { + apiRef.current.setRowCount( + paginationModel.pageSize * paginationModel.page + paginationModel.pageSize, + ); } }, [apiRef, props.paginationMode, rowCountState, paginationModel], From 2660ce9de7ad82d0288c8720b9c8e65f7eb9ce87 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 21 Mar 2024 12:48:15 +0500 Subject: [PATCH 09/27] Event update --- packages/x-data-grid/src/models/events/gridEventLookup.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/x-data-grid/src/models/events/gridEventLookup.ts b/packages/x-data-grid/src/models/events/gridEventLookup.ts index f6fa81651048..0ffc296f66fd 100644 --- a/packages/x-data-grid/src/models/events/gridEventLookup.ts +++ b/packages/x-data-grid/src/models/events/gridEventLookup.ts @@ -364,13 +364,13 @@ export interface GridControlledStateEventLookup { */ rowCountChange: { params: number }; /** - * Fired when the pagination meta change. - */ - paginationMetaChange: { params: GridPaginationMeta }; - /* * Fired when the density changes. */ densityChange: { params: GridDensity }; + /** + * Fired when the pagination meta change. + */ + paginationMetaChange: { params: GridPaginationMeta }; } export interface GridControlledStateReasonLookup { From a9254ecf1afe6bc107c916ae8b71b337998d1753 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 21 Mar 2024 16:05:01 +0500 Subject: [PATCH 10/27] Add tests --- .../src/tests/pagination.DataGrid.test.tsx | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx b/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx index fa636c43f242..46cc151be342 100644 --- a/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx @@ -10,6 +10,7 @@ import { GridRowsProp, GridApi, useGridApiRef, + GridPaginationMeta, } from '@mui/x-data-grid'; import { useBasicDemoData } from '@mui/x-data-grid-generator'; import { getCell, getColumnValues, getRows } from 'test/utils/helperFn'; @@ -322,8 +323,8 @@ describe(' - Pagination', () => { />, ); }).toWarnDev([ - `MUI X: The page size \`${pageSize}\` is not preset in the \`pageSizeOptions\``, - `MUI X: The page size \`${pageSize}\` is not preset in the \`pageSizeOptions\``, + `MUI X: The page size \`${pageSize}\` is not present in the \`pageSizeOptions\``, + `MUI X: The page size \`${pageSize}\` is not present in the \`pageSizeOptions\``, ]); }); @@ -350,8 +351,8 @@ describe(' - Pagination', () => { expect(() => { render(); }).toWarnDev([ - `MUI X: The page size \`${pageSize}\` is not preset in the \`pageSizeOptions\``, - `MUI X: The page size \`${pageSize}\` is not preset in the \`pageSizeOptions\``, + `MUI X: The page size \`${pageSize}\` is not present in the \`pageSizeOptions\``, + `MUI X: The page size \`${pageSize}\` is not present in the \`pageSizeOptions\``, ]); }); @@ -359,8 +360,8 @@ describe(' - Pagination', () => { expect(() => { render(); }).toWarnDev([ - `MUI X: The page size \`100\` is not preset in the \`pageSizeOptions\``, - `MUI X: The page size \`100\` is not preset in the \`pageSizeOptions\``, + `MUI X: The page size \`100\` is not present in the \`pageSizeOptions\``, + `MUI X: The page size \`100\` is not present in the \`pageSizeOptions\``, ]); }); @@ -538,8 +539,8 @@ describe(' - Pagination', () => { expect(document.querySelector('.MuiTablePagination-root')).to.have.text('1–1 of 21'); }); - it('should support server side pagination', () => { - function ServerPaginationGrid() { + describe('server-side pagination', () => { + function ServerPaginationGrid(props: Partial) { const [rows, setRows] = React.useState([]); const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 1 }); @@ -576,20 +577,52 @@ describe(' - Pagination', () => {
); } - render(); - expect(getColumnValues(0)).to.deep.equal(['0']); - fireEvent.click(screen.getByRole('button', { name: /next page/i })); - expect(getColumnValues(0)).to.deep.equal(['1']); + it('should support server side pagination with known row count', () => { + render(); + expect(getColumnValues(0)).to.deep.equal(['0']); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + expect(getColumnValues(0)).to.deep.equal(['1']); + }); + + it('should support server side pagination with unknown row count', () => { + const { setProps } = render(); + expect(getColumnValues(0)).to.deep.equal(['0']); + expect(screen.getByText('1–1 of more than 1')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + expect(getColumnValues(0)).to.deep.equal(['1']); + expect(screen.getByText('2–2 of more than 2')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + setProps({ rowCount: 3 }); + expect(getColumnValues(0)).to.deep.equal(['2']); + expect(screen.getByText('3–3 of 3')).to.not.equal(null); + }); + + it('should support server side pagination with estimated row count', () => { + const { setProps } = render(); + expect(getColumnValues(0)).to.deep.equal(['0']); + expect(screen.getByText('1–1 of more than 2')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + expect(getColumnValues(0)).to.deep.equal(['1']); + expect(screen.getByText('2–2 of more than 2')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + expect(getColumnValues(0)).to.deep.equal(['2']); + expect(screen.getByText('3–3 of more than 3')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + setProps({ rowCount: 4 }); + expect(getColumnValues(0)).to.deep.equal(['3']); + expect(screen.getByText('4–4 of 4')).to.not.equal(null); + }); }); it('should make the first cell focusable after changing the page', () => { From 67b4f4b438c2e98331f55adfd3e674cb18cae076 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 21 Mar 2024 16:15:51 +0500 Subject: [PATCH 11/27] Lint --- packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx b/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx index 46cc151be342..53977eda938d 100644 --- a/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx @@ -10,7 +10,6 @@ import { GridRowsProp, GridApi, useGridApiRef, - GridPaginationMeta, } from '@mui/x-data-grid'; import { useBasicDemoData } from '@mui/x-data-grid-generator'; import { getCell, getColumnValues, getRows } from 'test/utils/helperFn'; From 157bc46d34445018fa7be8340c8072f7b5ae417a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 2 Apr 2024 09:33:57 +0500 Subject: [PATCH 12/27] docs:api --- docs/pages/x/api/data-grid/grid-api.json | 4 ++++ docs/translations/api-docs/data-grid/grid-api.json | 1 + 2 files changed, 5 insertions(+) diff --git a/docs/pages/x/api/data-grid/grid-api.json b/docs/pages/x/api/data-grid/grid-api.json index 4acb3f3986bb..a4fc87914cd1 100644 --- a/docs/pages/x/api/data-grid/grid-api.json +++ b/docs/pages/x/api/data-grid/grid-api.json @@ -346,6 +346,10 @@ }, "setPage": { "type": { "description": "(page: number) => void" }, "required": true }, "setPageSize": { "type": { "description": "(pageSize: number) => void" }, "required": true }, + "setPaginationMeta": { + "type": { "description": "(paginationMeta: GridPaginationMeta) => void" }, + "required": true + }, "setPaginationModel": { "type": { "description": "(model: GridPaginationModel) => void" }, "required": true diff --git a/docs/translations/api-docs/data-grid/grid-api.json b/docs/translations/api-docs/data-grid/grid-api.json index ba27c6f79b3a..02e89d3757ea 100644 --- a/docs/translations/api-docs/data-grid/grid-api.json +++ b/docs/translations/api-docs/data-grid/grid-api.json @@ -184,6 +184,7 @@ "setPageSize": { "description": "Sets the number of displayed rows to the value given by pageSize." }, + "setPaginationMeta": { "description": "Sets the paginationMeta to a new value." }, "setPaginationModel": { "description": "Sets the paginationModel to a new value." }, From 27e38d1b6a57dd17e4581503162ce8912ba27bcf Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 2 Apr 2024 11:36:25 +0500 Subject: [PATCH 13/27] Allow localization for estimated use-case + docs improvement --- .../data-grid/localization/localization.md | 2 +- docs/data/data-grid/pagination/pagination.md | 66 ++++++++++++++----- .../src/components/GridPagination.tsx | 4 +- packages/x-data-grid/src/components/index.ts | 2 +- .../src/hooks/utils/useGridSelector.ts | 2 +- .../src/models/api/gridApiCommon.ts | 2 +- .../src/models/api/gridLocaleTextApi.ts | 17 +++-- packages/x-data-grid/src/models/api/index.ts | 2 +- .../src/utils/getGridLocalization.ts | 9 ++- 9 files changed, 74 insertions(+), 32 deletions(-) diff --git a/docs/data/data-grid/localization/localization.md b/docs/data/data-grid/localization/localization.md index 117d8792a96d..28197aed4c87 100644 --- a/docs/data/data-grid/localization/localization.md +++ b/docs/data/data-grid/localization/localization.md @@ -24,7 +24,7 @@ One example is the table pagination component used in the Data Grid footer when localeText={{ MuiTablePagination: { labelDisplayedRows: ({ from, to, count }) => - `${from} - ${to} of more than ${count}`, + `${from} - ${to} of ${count === -1 ? `more than ${to}` : count}`, }, }} /> diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index 3e01127a7888..839fdd5198a8 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -94,9 +94,9 @@ const [paginationModel, setPaginationModel] = React.useState({ By default, the pagination is handled on the client. This means you have to give the rows of all pages to the data grid. -If your dataset is too big, and you only want to fetch the current page, you can use server-side pagination. +If your dataset is too big, and you want to fetch the pages on demand, you can use server-side pagination. -Grid supports two types of server-side pagination based on the supported API from the server: +In general, the server-side pagination could be categorized into two types: - Index-based pagination - Cursor-based pagination @@ -111,7 +111,8 @@ The index-based pagination is the most common type of pagination. It follows the It can be further categorized into sub-types based on the availability of the total number of rows or `rowCount`. -You can provide the `rowCount` using one of the following ways: +The Grid uses the `rowCount` to calculate the number of pages and to show the information about the current state of the pagination in the footer. +You can provide the `rowCount` in one of the following ways: - Initialize - use `initialState.pagination.rowCount` prop to initialize the `rowCount` - Control - use the `rowCount` prop along with `onRowCountChange` to control the `rowCount` and reflect the change it when the row count is updated @@ -119,19 +120,19 @@ You can provide the `rowCount` using one of the following ways: There can be three different possibilities regarding the availability of the `rowCount` on the server-side: -1. Row count is available -2. Row count is not available -3. Row count is available but is not accurate and may update later on +1. Row count is available (known) +2. Row count is not available (unknown) +3. Row count is available but is not accurate and may update later on (estimated) The following sections will guide you through the implementation of each of these possibilities. -#### 1. Row count is available +#### Known row count -When the row count is available, you can use the following steps to enable server-side pagination: +When the row count is available from the beginning, you can use the following steps to enable server-side pagination: - Set the prop `paginationMode` to `server` - Provide the `rowCount` value using one of the above mentioned ways -- Use the `onPaginationModelChange` prop to load the rows when the page changes +- Use the `onPaginationModelChange` prop to react to the page changes and load the data from the server Since the `rowCount` prop is used to compute the number of available pages, switching it to `undefined` during loading resets the page to zero. To avoid this problem, you can keep the previous value of `rowCount` while loading as follows: @@ -149,7 +150,7 @@ React.useEffect(() => { {{"demo": "ServerPaginationGrid.js", "bg": "inline"}} -#### 2. Row count is not available +#### Unknown row count When the row count is not available, you can use the following steps to enable server-side pagination: @@ -158,13 +159,13 @@ When the row count is not available, you can use the following steps to enable s - Since the `rowCount` is unknown, the Grid needs to know whether more records are available on the server. Pass the `paginationMeta.hasNextPage` boolean property to indicate whether more records are available. - Use the `onPaginationModelChange` prop to react to the `paginationModel` changes -The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value. +The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value to a finite value. Relying on this behavior of the Grid should be the last resort though, the preferred way is to set the `rowCount` to the actual number of rows fetched from the server as soon as the information is available since the Grid cannot guess if the rows in the last page are less than the page size, and the incorrect number might be displayed in the pagination UI. -For example the if the row count is `15` and the page size is `10`, on the last page, when `hasNextPage` becomes false the Grid will set the row count to be `page * pageSize` (20) which will show `10-20 of 20` which is incorrect, the correct value should be `10-15 of 15`. +For example the if the row count is `15` and the page size is `10`, on the last page, when `hasNextPage` becomes false the Grid will set the row count to be `page (2) * pageSize (10) = (20)` showing `10-20 of 20` which is incorrect, the correct value should be `10-15 of 15`. -The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook, one possible solution could be to memoize the `paginationMeta` value to avoid unnecessary flickering of the pagination UI: +The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook resulting in flickering of the pagination UI, one possible solution could be to memoize the `paginationMeta`: ```tsx const paginationMetaRef = React.useRef(); @@ -182,14 +183,14 @@ const paginationMeta = React.useMemo(() => { {{"demo": "ServerPaginationGridNoRowCount.js", "bg": "inline"}} -#### 3. Row count is available but is not accurate +#### Estimated row count There could be possibilities when the accurate row count is not initially available for many reasons such as: 1. For some databases, computing `rowCount` upfront is a costly operation due to the scale of data or how the data is structured. 2. Some data structures don't have the `rowCount` information until the very last page. -In such cases, the backend could send an estimated row count which could be initially used to calculate the number of pages. In some cases, this estimate value could also be more than the actual row count. So the Grid uses it only to show the user an estimated number of rows, but relies on the `hasNextPage` prop to check whether more records are available on the server. +In such scenarios, some backends provide an estimated row count initially which could be used to estimate the number of pages until the actual row count is available. In some cases, this estimate value could also be more than the actual row count. Therefore, the Grid uses it only to show the user an estimated number of rows, and uses `paginationMeta.hasNextPage` as a single source of truth to check whether more records are available on the server. Here are the steps to achieve it: @@ -201,13 +202,44 @@ Here are the steps to achieve it: There could be two further possibilities: 1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a positive value. -2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage=false` to know that the last page is fetched and try to set a value. Option 1 should be the preferred way to solve this though. +2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage=false` to know that the last page is fetched and try to set a value like in the unknown row count use case. Option 1 should be the preferred way to solve this though. The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Set Row Count" button. {{"demo": "ServerPaginationGridTruncated.js", "bg": "inline"}} -### Cursor implementation +::: + +🌍 **Localization of the estimated row count** + +The Data Grid uses the [Table Pagination](/material-ui/api/table-pagination/) component from the Material UI library which doesn't support `estimated` row count. Until this is supported natively by the Table Pagination component, a workaround to make the localization work is to provide the `labelDisplayedRows` function to the `localeText.MuiTablePagination` property as per the locale you are interested in. + +The Grid injects an additional variable `estimated` to the `labelDisplayedRows` function which you can use to accomodate the estimated row count. +The following example demonstrates how to show the estimated row count in the pagination footer in the Croatian (hr-HR) language. + +```jsx +const labelDisplayedRows = ({ from, to, count, estimated }) => { + if (!estimated) { + return `${from}–${to} od ${count !== -1 ? count : `više nego ${to}`}`, + } + return `${from}–${to} od ${count !== -1 ? count : `više nego ${estimated > to ? estimated : to}`}`; +} + + +``` + +For more information, see [Translation keys](/x/react-data-grid/localization/#translation-keys) section of the localization documentation. + +::: + +### Cursor-based pagination You can also handle servers with cursor-based pagination. To do so, you just have to keep track of the next cursor associated with each page you fetched. diff --git a/packages/x-data-grid/src/components/GridPagination.tsx b/packages/x-data-grid/src/components/GridPagination.tsx index 0274484d1de9..6a97e3ac810f 100644 --- a/packages/x-data-grid/src/components/GridPagination.tsx +++ b/packages/x-data-grid/src/components/GridPagination.tsx @@ -30,7 +30,7 @@ const GridPaginationRoot = styled(TablePagination)(({ theme }) => ({ }, })) as typeof TablePagination; -type WrappedLabelDisplayedRows = ( +export type WrappedLabelDisplayedRows = ( args: LabelDisplayedRowsArgs & { estimated?: number }, ) => React.ReactNode; @@ -39,7 +39,7 @@ const wrapLabelDisplayedRows = ( estimated?: number, ): TablePaginationProps['labelDisplayedRows'] => { return ({ from, to, count, page }: LabelDisplayedRowsArgs) => - labelDisplayedRows!({ from, to, count, page, estimated }); + labelDisplayedRows({ from, to, count, page, estimated }); }; const defaultLabelDisplayedRows: WrappedLabelDisplayedRows = ({ from, to, count, estimated }) => { diff --git a/packages/x-data-grid/src/components/index.ts b/packages/x-data-grid/src/components/index.ts index 6313a66231b5..2657dac0cd89 100644 --- a/packages/x-data-grid/src/components/index.ts +++ b/packages/x-data-grid/src/components/index.ts @@ -14,7 +14,7 @@ export * from './GridFooter'; export * from './GridHeader'; export * from './GridLoadingOverlay'; export * from './GridNoRowsOverlay'; -export * from './GridPagination'; +export { GridPagination } from './GridPagination'; export * from './GridRowCount'; export * from './GridRow'; export * from './GridSelectedRowCount'; diff --git a/packages/x-data-grid/src/hooks/utils/useGridSelector.ts b/packages/x-data-grid/src/hooks/utils/useGridSelector.ts index a5337ea3c74c..859167dcfb24 100644 --- a/packages/x-data-grid/src/hooks/utils/useGridSelector.ts +++ b/packages/x-data-grid/src/hooks/utils/useGridSelector.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { GridApiCommon } from '../../models/api/gridApiCommon'; +import type { GridApiCommon } from '../../models/api/gridApiCommon'; import { OutputSelector } from '../../utils/createSelector'; import { useLazyRef } from './useLazyRef'; import { useOnMount } from './useOnMount'; diff --git a/packages/x-data-grid/src/models/api/gridApiCommon.ts b/packages/x-data-grid/src/models/api/gridApiCommon.ts index 05cf1bd6bda7..1e59d123c8a5 100644 --- a/packages/x-data-grid/src/models/api/gridApiCommon.ts +++ b/packages/x-data-grid/src/models/api/gridApiCommon.ts @@ -6,7 +6,7 @@ import { GridDensityApi } from './gridDensityApi'; import { GridEditingApi, GridEditingPrivateApi } from './gridEditingApi'; import type { GridFilterApi } from './gridFilterApi'; import { GridFocusApi, GridFocusPrivateApi } from './gridFocusApi'; -import { GridLocaleTextApi } from './gridLocaleTextApi'; +import type { GridLocaleTextApi } from './gridLocaleTextApi'; import type { GridParamsApi } from './gridParamsApi'; import { GridPreferencesPanelApi } from './gridPreferencesPanelApi'; import { GridPrintExportApi } from './gridPrintExportApi'; diff --git a/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts b/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts index b25858988c0a..97735a52eb0f 100644 --- a/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts +++ b/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts @@ -1,6 +1,14 @@ import * as React from 'react'; -import { ComponentsPropsList } from '@mui/material/styles'; -import { GridColDef } from '../colDef'; +import type { ComponentsPropsList } from '@mui/material/styles'; +import type { WrappedLabelDisplayedRows } from '../../components/GridPagination'; +import type { GridColDef } from '../colDef'; + +export type MuiTablePaginationLocalizedProps = Omit< + ComponentsPropsList['MuiTablePagination'], + 'page' | 'count' | 'onChangePage' | 'rowsPerPage' | 'onPageChange' | 'labelDisplayedRows' +> & { + labelDisplayedRows?: WrappedLabelDisplayedRows; +}; /** * Set the types of the texts in the grid. @@ -174,10 +182,7 @@ export interface GridLocaleText { aggregationFunctionLabelSize: string; // Used core components translation keys - MuiTablePagination: Omit< - ComponentsPropsList['MuiTablePagination'], - 'page' | 'count' | 'onChangePage' | 'rowsPerPage' | 'onPageChange' - >; + MuiTablePagination: MuiTablePaginationLocalizedProps; } export type GridTranslationKeys = keyof GridLocaleText; diff --git a/packages/x-data-grid/src/models/api/index.ts b/packages/x-data-grid/src/models/api/index.ts index 16cae502d7ec..c534577dffac 100644 --- a/packages/x-data-grid/src/models/api/index.ts +++ b/packages/x-data-grid/src/models/api/index.ts @@ -9,7 +9,7 @@ export type { GridRowsMetaApi } from './gridRowsMetaApi'; export * from './gridRowSelectionApi'; export * from './gridSortApi'; export type { GridStateApi } from './gridStateApi'; -export * from './gridLocaleTextApi'; +export { GridLocaleText, GridLocaleTextApi, GridTranslationKeys } from './gridLocaleTextApi'; export * from './gridCsvExportApi'; export type { GridFocusApi } from './gridFocusApi'; export * from './gridFilterApi'; diff --git a/packages/x-data-grid/src/utils/getGridLocalization.ts b/packages/x-data-grid/src/utils/getGridLocalization.ts index 8bc8e2f22c61..e28c5c454327 100644 --- a/packages/x-data-grid/src/utils/getGridLocalization.ts +++ b/packages/x-data-grid/src/utils/getGridLocalization.ts @@ -1,5 +1,8 @@ import { Localization as CoreLocalization } from '@mui/material/locale'; -import { GridLocaleText } from '../models/api/gridLocaleTextApi'; +import type { + GridLocaleText, + MuiTablePaginationLocalizedProps, +} from '../models/api/gridLocaleTextApi'; export interface Localization { components: { @@ -20,7 +23,9 @@ export const getGridLocalization = ( defaultProps: { localeText: { ...gridTranslations, - MuiTablePagination: coreTranslations?.components?.MuiTablePagination?.defaultProps || {}, + MuiTablePagination: + (coreTranslations?.components?.MuiTablePagination + ?.defaultProps as MuiTablePaginationLocalizedProps) || {}, }, }, }, From 1309497373c9cfc00c2b014e7a63617253f6091c Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 2 Apr 2024 11:46:10 +0500 Subject: [PATCH 14/27] test_types fix --- packages/x-data-grid/src/models/api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-data-grid/src/models/api/index.ts b/packages/x-data-grid/src/models/api/index.ts index c534577dffac..38a051fc1f05 100644 --- a/packages/x-data-grid/src/models/api/index.ts +++ b/packages/x-data-grid/src/models/api/index.ts @@ -9,7 +9,7 @@ export type { GridRowsMetaApi } from './gridRowsMetaApi'; export * from './gridRowSelectionApi'; export * from './gridSortApi'; export type { GridStateApi } from './gridStateApi'; -export { GridLocaleText, GridLocaleTextApi, GridTranslationKeys } from './gridLocaleTextApi'; +export type { GridLocaleText, GridLocaleTextApi, GridTranslationKeys } from './gridLocaleTextApi'; export * from './gridCsvExportApi'; export type { GridFocusApi } from './gridFocusApi'; export * from './gridFilterApi'; From 18ae445e91dbdd7f3355e15c657d188efd1edd42 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 5 Apr 2024 06:36:23 +0500 Subject: [PATCH 15/27] Improvement --- docs/data/data-grid/pagination/pagination.md | 47 ++++++++----------- .../src/hooks/useQuery.ts | 2 +- .../src/internals/utils/propValidation.ts | 9 ++-- .../src/models/props/DataGridProps.ts | 4 +- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index e97c46eef566..ce5756258d84 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -109,7 +109,12 @@ Check out [Selection—Usage with server-side pagination](/x/react-data-grid/row The index-based pagination is the most common type of pagination. It follows the same pattern as the client-side pagination (page, pageSize), but the data is fetched from the server. -It can be further categorized into sub-types based on the availability of the total number of rows or `rowCount`. +To enable server-side pagination, you need to: + +- Set the `paginationMode` prop to `server` +- Use the `onPaginationModelChange` prop to react to the page changes and load the data from the server + +The server-side pagination can be further categorized into sub-types based on the availability of the total number of rows or `rowCount`. The Grid uses the `rowCount` to calculate the number of pages and to show the information about the current state of the pagination in the footer. You can provide the `rowCount` in one of the following ways: @@ -124,15 +129,22 @@ There can be three different possibilities regarding the availability of the `ro 2. Row count is not available (unknown) 3. Row count is available but is not accurate and may update later on (estimated) -The following sections will guide you through the implementation of each of these possibilities. +:::warning +The `rowCount` prop is used in server-side pagination mode to inform the DataGrid about the total number of rows in your dataset. +This prop is ignored when the `paginationMode` is set to `client`, i.e. when the pagination is handled on the client-side. +::: + +You can achieve each of these use-cases by setting the values of `rowCount`, `paginationMeta.hasNextPage`, and `estimatedRowCount` props. The following table summarizes the use of these props in these scenarios: -#### Known row count +| | Known row count | Unknown row count | Estimated row count | +| :--------------------------- | :-------------- | :---------------- | :------------------ | +| `rowCount` | `number` | `-1` | `-1` | +| `paginationMeta.hasNextPage` | — | `boolean` | `boolean` | +| `estimatedRowCount` | — | — | `number` | -When the row count is available from the beginning, you can use the following steps to enable server-side pagination: +The following examples demonstrate how to handle each of these scenarios: -- Set the prop `paginationMode` to `server` -- Provide the `rowCount` value using one of the above mentioned ways -- Use the `onPaginationModelChange` prop to react to the page changes and load the data from the server +#### Known row count Since the `rowCount` prop is used to compute the number of available pages, switching it to `undefined` during loading resets the page to zero. To avoid this problem, you can keep the previous value of `rowCount` while loading as follows: @@ -148,22 +160,10 @@ React.useEffect(() => { ; ``` -:::warning -The `rowCount` prop is used in server-side pagination mode to inform the DataGrid about the total number of rows in your dataset. -This prop is ignored when the `paginationMode` is set to `client`, i.e. when the pagination is handled on the client-side. -::: - {{"demo": "ServerPaginationGrid.js", "bg": "inline"}} #### Unknown row count -When the row count is not available, you can use the following steps to enable server-side pagination: - -- Set the prop `paginationMode` to `server` -- Set the `rowCount` as `-1` using one of the above mentioned ways to let the Grid know that the row count is not available -- Since the `rowCount` is unknown, the Grid needs to know whether more records are available on the server. Pass the `paginationMeta.hasNextPage` boolean property to indicate whether more records are available. -- Use the `onPaginationModelChange` prop to react to the `paginationModel` changes - The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value to a finite value. Relying on this behavior of the Grid should be the last resort though, the preferred way is to set the `rowCount` to the actual number of rows fetched from the server as soon as the information is available since the Grid cannot guess if the rows in the last page are less than the page size, and the incorrect number might be displayed in the pagination UI. @@ -197,14 +197,7 @@ There could be possibilities when the accurate row count is not initially availa In such scenarios, some backends provide an estimated row count initially which could be used to estimate the number of pages until the actual row count is available. In some cases, this estimate value could also be more than the actual row count. Therefore, the Grid uses it only to show the user an estimated number of rows, and uses `paginationMeta.hasNextPage` as a single source of truth to check whether more records are available on the server. -Here are the steps to achieve it: - -- Set the prop `paginationMode` to `server` -- Pass the estimated row count using the `estimatedRowCount` prop and set the `rowCount` to `-1` to indicate that the actual row count is not (yet) available. -- Pass the `paginationMeta.hasNextPage` to let the Grid check on-demand whether more records are available on the server, the Grid will keep fetching the next page until `hasNextPage` is `false` -- Use the `onPaginationModelChange` prop to react to the `paginationModel` changes - -There could be two further possibilities: +There could be two possibilities after providing the initial props: 1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a positive value. 2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage=false` to know that the last page is fetched and try to set a value like in the unknown row count use case. Option 1 should be the preferred way to solve this though. diff --git a/packages/x-data-grid-generator/src/hooks/useQuery.ts b/packages/x-data-grid-generator/src/hooks/useQuery.ts index 9259061ecb55..55adae6cb6a7 100644 --- a/packages/x-data-grid-generator/src/hooks/useQuery.ts +++ b/packages/x-data-grid-generator/src/hooks/useQuery.ts @@ -146,7 +146,7 @@ export const loadServerRows = ( firstRowIndex = page * pageSize; lastRowIndex = (page + 1) * pageSize; } - const hasNextPage = lastRowIndex < filteredRows.length; + const hasNextPage = lastRowIndex < filteredRows.length - 1; const response: FakeServerResponse = { returnedRows: filteredRows.slice(firstRowIndex, lastRowIndex), nextCursor, diff --git a/packages/x-data-grid/src/internals/utils/propValidation.ts b/packages/x-data-grid/src/internals/utils/propValidation.ts index dba40c03329f..46e68732289c 100644 --- a/packages/x-data-grid/src/internals/utils/propValidation.ts +++ b/packages/x-data-grid/src/internals/utils/propValidation.ts @@ -23,10 +23,11 @@ export const propValidatorsDataGrid: PropValidator[] = [ 'Consider removing the `paginationMeta` prop or switching to `server` mode if your pagination is on server.', ].join('\n')) || undefined, - (props.signature === GridSignature.DataGrid && - props.paginationMode === 'client' && - 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"`.') || + (props) => + (props.signature === GridSignature.DataGrid && + props.paginationMode === 'client' && + 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, ]; diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 3eb6c7ee7074..170ee2e261e1 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -411,13 +411,13 @@ export interface DataGridPropsWithoutDefaultValue Date: Fri, 5 Apr 2024 15:14:32 +0500 Subject: [PATCH 16/27] Accommodate inconsistent last page row length --- .../pagination/ServerPaginationGrid.js | 22 +++++---- .../pagination/ServerPaginationGrid.tsx | 22 +++++---- .../ServerPaginationGrid.tsx.preview | 2 +- ...ed.js => ServerPaginationGridEstimated.js} | 4 +- ....tsx => ServerPaginationGridEstimated.tsx} | 4 +- ...ServerPaginationGridEstimated.tsx.preview} | 2 +- docs/data/data-grid/pagination/pagination.md | 45 ++++++++++--------- .../features/pagination/useGridRowCount.ts | 38 ++++++++-------- 8 files changed, 70 insertions(+), 69 deletions(-) rename docs/data/data-grid/pagination/{ServerPaginationGridTruncated.js => ServerPaginationGridEstimated.js} (94%) rename docs/data/data-grid/pagination/{ServerPaginationGridTruncated.tsx => ServerPaginationGridEstimated.tsx} (94%) rename docs/data/data-grid/pagination/{ServerPaginationGridTruncated.tsx.preview => ServerPaginationGridEstimated.tsx.preview} (92%) diff --git a/docs/data/data-grid/pagination/ServerPaginationGrid.js b/docs/data/data-grid/pagination/ServerPaginationGrid.js index 0bcb826f4034..6b142879e4f9 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGrid.js +++ b/docs/data/data-grid/pagination/ServerPaginationGrid.js @@ -17,24 +17,22 @@ export default function ServerPaginationGrid() { const { isLoading, rows, pageInfo } = useQuery(paginationModel); // Some API clients return undefined while loading - // Following lines are here to prevent `rowCountState` from being undefined during the loading - const [rowCountState, setRowCountState] = React.useState( - pageInfo?.totalRowCount || 0, - ); - React.useEffect(() => { - setRowCountState((prevRowCountState) => - pageInfo?.totalRowCount !== undefined - ? pageInfo?.totalRowCount - : prevRowCountState, - ); - }, [pageInfo?.totalRowCount, setRowCountState]); + // Following lines are here to prevent `rowCount` from being undefined during the loading + const rowCountRef = React.useRef(pageInfo?.totalRowCount || 0); + + const rowCount = React.useMemo(() => { + if (pageInfo?.totalRowCount !== undefined) { + rowCountRef.current = pageInfo.totalRowCount; + } + return rowCountRef.current; + }, [pageInfo?.totalRowCount]); return (
{ - setRowCountState((prevRowCountState) => - pageInfo?.totalRowCount !== undefined - ? pageInfo?.totalRowCount - : prevRowCountState, - ); - }, [pageInfo?.totalRowCount, setRowCountState]); + // Following lines are here to prevent `rowCount` from being undefined during the loading + const rowCountRef = React.useRef(pageInfo?.totalRowCount || 0); + + const rowCount = React.useMemo(() => { + if (pageInfo?.totalRowCount !== undefined) { + rowCountRef.current = pageInfo.totalRowCount; + } + return rowCountRef.current; + }, [pageInfo?.totalRowCount]); return (
{ - setRowCountState((prevRowCountState) => - rowCount !== undefined ? rowCount : prevRowCountState, - ); -}, [rowCount, setRowCountState]); - -; +const rowCountRef = React.useRef(pageInfo?.totalRowCount || 0); + +const rowCount = React.useMemo(() => { + if (pageInfo?.totalRowCount !== undefined) { + rowCountRef.current = pageInfo.totalRowCount; + } + return rowCountRef.current; +}, [pageInfo?.totalRowCount]); + +; ``` {{"demo": "ServerPaginationGrid.js", "bg": "inline"}} #### Unknown row count -The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value to a finite value. +Pass the props to the Grid as explained in the table above to handle the case when the actual row count is not initially available. -Relying on this behavior of the Grid should be the last resort though, the preferred way is to set the `rowCount` to the actual number of rows fetched from the server as soon as the information is available since the Grid cannot guess if the rows in the last page are less than the page size, and the incorrect number might be displayed in the pagination UI. - -For example the if the row count is `15` and the page size is `10`, on the last page, when `hasNextPage` becomes false the Grid will set the row count to be `page (2) * pageSize (10) = (20)` showing `10-20 of 20` which is incorrect, the correct value should be `10-15 of 15`. +:::warning +The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value to a finite value. +::: -The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook resulting in flickering of the pagination UI, one possible solution could be to memoize the `paginationMeta`: +The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook resulting in unwanted computations, one possible solution could be to memoize the `paginationMeta`: ```tsx const paginationMetaRef = React.useRef(); @@ -197,14 +200,14 @@ There could be possibilities when the accurate row count is not initially availa In such scenarios, some backends provide an estimated row count initially which could be used to estimate the number of pages until the actual row count is available. In some cases, this estimate value could also be more than the actual row count. Therefore, the Grid uses it only to show the user an estimated number of rows, and uses `paginationMeta.hasNextPage` as a single source of truth to check whether more records are available on the server. -There could be two possibilities after providing the initial props: +There could be two possibilities after providing the initial props (as mentioned in the table above): 1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a positive value. -2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage=false` to know that the last page is fetched and try to set a value like in the unknown row count use case. Option 1 should be the preferred way to solve this though. +2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage='false'` to know that the last page is fetched and try to set a value like in the unknown row count use case. The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Set Row Count" button. -{{"demo": "ServerPaginationGridTruncated.js", "bg": "inline"}} +{{"demo": "ServerPaginationGridEstimated.js", "bg": "inline"}} ::: diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts index 653204c0bf95..a5dbdba3e527 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts @@ -33,6 +33,7 @@ export const useGridRowCount = ( const paginationMeta = useGridSelector(apiRef, gridPaginationMetaSelector); const paginationModel = useGridSelector(apiRef, gridPaginationModelSelector); const previousPageSize = useLazyRef(() => gridPaginationModelSelector(apiRef).pageSize); + const prevRowCountProp = React.useRef(props.rowCount); apiRef.current.registerControlState({ stateId: 'paginationRowCount', @@ -122,21 +123,6 @@ export const useGridRowCount = ( /** * EVENTS */ - const handlePaginationMetaChange = React.useCallback( - (meta: GridPaginationState['meta']) => { - if (props.paginationMode === 'client') { - return; - } - - if (!meta.hasNextPage && rowCountState === -1) { - apiRef.current.setRowCount( - paginationModel.pageSize * paginationModel.page + paginationModel.pageSize, - ); - } - }, - [apiRef, props.paginationMode, rowCountState, paginationModel], - ); - const handlePaginationModelChange = React.useCallback( (model: GridPaginationState['paginationModel']) => { if (props.paginationMode === 'client' || !previousPageSize.current) { @@ -160,7 +146,6 @@ export const useGridRowCount = ( ); useGridApiEventHandler(apiRef, 'paginationModelChange', handlePaginationModelChange); - useGridApiEventHandler(apiRef, 'paginationMetaChange', handlePaginationMetaChange); /** * EFFECTS @@ -179,15 +164,32 @@ export const useGridRowCount = ( return; } - if (isNumber(props.rowCount)) { + if (isNumber(props.rowCount) && prevRowCountProp.current !== props.rowCount) { + prevRowCountProp.current = props.rowCount; apiRef.current.setRowCount(props.rowCount); + return; + } + + const isLastPage = paginationMeta.hasNextPage === false; + if ( + isLastPage && + (rowCountState === -1 || rowCountState % paginationModel.pageSize === 0) && + visibleTopLevelRowCount < paginationModel.pageSize + ) { + // Actual count of the rows is less than the page size, reflect the value + apiRef.current.setRowCount( + (rowCountState !== -1 + ? rowCountState - paginationModel.pageSize + : paginationModel.pageSize * paginationModel.page) + visibleTopLevelRowCount, + ); } }, [ apiRef, visibleTopLevelRowCount, props.paginationMode, props.rowCount, - paginationMeta, + paginationMeta.hasNextPage, paginationModel, + rowCountState, ]); }; From 2dfd0e1fe48349fe5cb8c6e4fd1b3d5a406fc83d Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 5 Apr 2024 15:53:05 +0500 Subject: [PATCH 17/27] Refactors --- .../data-grid-premium/data-grid-premium.json | 4 ++-- .../data-grid/data-grid-pro/data-grid-pro.json | 4 ++-- .../api-docs/data-grid/data-grid/data-grid.json | 4 ++-- .../src/DataGridPremium/DataGridPremium.tsx | 4 ++-- .../src/DataGridPro/DataGridPro.tsx | 4 ++-- .../tests/statePersistence.DataGridPro.test.tsx | 1 - packages/x-data-grid/src/DataGrid/DataGrid.tsx | 4 ++-- .../features/pagination/gridPaginationUtils.ts | 8 -------- .../features/pagination/useGridRowCount.ts | 12 +----------- .../src/internals/utils/propValidation.ts | 17 ++++++++++++++--- 10 files changed, 27 insertions(+), 35 deletions(-) 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 bcb6df16e214..fbe690842af4 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 @@ -105,7 +105,7 @@ }, "editMode": { "description": "Controls whether to use the cell or row editing." }, "estimatedRowCount": { - "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with the server-side data and when rowCount="-1"" + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with paginationMode="server" and when rowCount="-1"" }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, then the feature is fully disabled, and neither property nor method calls will have any effect." @@ -588,7 +588,7 @@ "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Works only with the server-side data. Ignored when paginationMode === 'client'" + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Only works with paginationMode="server", ignored when paginationMode="client"." }, "rowGroupingColumnMode": { "description": "If single, all the columns that are grouped are represented in the same grid column. If multiple, each column that is grouped is represented in its own grid column." 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 7c6a0f707ebc..113220858b36 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 @@ -93,7 +93,7 @@ }, "editMode": { "description": "Controls whether to use the cell or row editing." }, "estimatedRowCount": { - "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with the server-side data and when rowCount="-1"" + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with paginationMode="server" and when rowCount="-1"" }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." @@ -534,7 +534,7 @@ "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Works only with the server-side data. Ignored when paginationMode === 'client'" + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Only works with paginationMode="server", ignored when paginationMode="client"." }, "rowHeight": { "description": "Sets the height in pixel of a row in the Data Grid." }, "rowModesModel": { "description": "Controls the modes of the rows." }, diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index c7efa6954968..c4924176b374 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -68,7 +68,7 @@ }, "editMode": { "description": "Controls whether to use the cell or row editing." }, "estimatedRowCount": { - "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with the server-side data and when rowCount="-1"" + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with paginationMode="server" and when rowCount="-1"" }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." @@ -437,7 +437,7 @@ "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Works only with the server-side data. Ignored when paginationMode === 'client'" + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Only works with paginationMode="server", ignored when paginationMode="client"." }, "rowHeight": { "description": "Sets the height in pixel of a row in the Data Grid." }, "rowModesModel": { "description": "Controls the modes of the rows." }, diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 961f149c3f67..1b67680f5fec 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -316,7 +316,7 @@ DataGridPremiumRaw.propTypes = { /** * Use if the actual rowCount is not known upfront, but an estimation is available. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. - * Applicable only with the server-side data and when `rowCount="-1"` + * Applicable only with `paginationMode="server"` and when `rowCount="-1"` */ estimatedRowCount: PropTypes.number, /** @@ -914,7 +914,7 @@ DataGridPremiumRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. - * Works only with the server-side data. Ignored when `paginationMode === 'client'` + * Only works with `paginationMode="server"`, ignored when `paginationMode="client"`. */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index da233a823993..49ecc70449ee 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -264,7 +264,7 @@ DataGridProRaw.propTypes = { /** * Use if the actual rowCount is not known upfront, but an estimation is available. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. - * Applicable only with the server-side data and when `rowCount="-1"` + * Applicable only with `paginationMode="server"` and when `rowCount="-1"` */ estimatedRowCount: PropTypes.number, /** @@ -816,7 +816,7 @@ DataGridProRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. - * Works only with the server-side data. Ignored when `paginationMode === 'client'` + * Only works with `paginationMode="server"`, ignored when `paginationMode="client"`. */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx index 65d81eeaa1e9..7ad8ff9e7d65 100644 --- a/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx @@ -202,7 +202,6 @@ describe(' - State persistence', () => { }, preferencePanel: FULL_INITIAL_STATE.preferencePanel, }} - paginationMode="server" />, ); expect(apiRef.current.exportState({ exportOnlyDirtyModels: true })).to.deep.equal( diff --git a/packages/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/x-data-grid/src/DataGrid/DataGrid.tsx index 0cf87d77f345..56a9868fa182 100644 --- a/packages/x-data-grid/src/DataGrid/DataGrid.tsx +++ b/packages/x-data-grid/src/DataGrid/DataGrid.tsx @@ -223,7 +223,7 @@ DataGridRaw.propTypes = { /** * Use if the actual rowCount is not known upfront, but an estimation is available. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. - * Applicable only with the server-side data and when `rowCount="-1"` + * Applicable only with `paginationMode="server"` and when `rowCount="-1"` */ estimatedRowCount: PropTypes.number, /** @@ -683,7 +683,7 @@ DataGridRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. - * Works only with the server-side data. Ignored when `paginationMode === 'client'` + * Only works with `paginationMode="server"`, ignored when `paginationMode="client"`. */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts index 601822730c88..225d75a88539 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts @@ -18,14 +18,6 @@ export const getPageCount = (rowCount: number, pageSize: number, page: number): return 0; }; -export const noRowCountInServerMode = buildWarning( - [ - "MUI X: the 'rowCount' prop is undefined while using paginationMode='server'", - 'For more detail, see http://mui.com/components/data-grid/pagination/#basic-implementation', - ], - 'error', -); - export const getDefaultGridPaginationModel = (autoPageSize: boolean) => ({ page: 0, pageSize: autoPageSize ? 0 : 100, diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts index a5dbdba3e527..cc41ac93eacd 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts @@ -17,7 +17,6 @@ import { gridPaginationMetaSelector, gridPaginationModelSelector, } from './gridPaginationSelector'; -import { noRowCountInServerMode } from './gridPaginationUtils'; export const useGridRowCount = ( apiRef: React.MutableRefObject, @@ -129,9 +128,8 @@ export const useGridRowCount = ( return; } if (model.pageSize !== previousPageSize.current) { - // The page size has changed previousPageSize.current = model.pageSize; - if (rowCountState !== -1) { + if (rowCountState === -1) { // Row count unknown and page size changed, reset the page apiRef.current.setPage(0); return; @@ -150,14 +148,6 @@ export const useGridRowCount = ( /** * EFFECTS */ - React.useEffect(() => { - if (process.env.NODE_ENV !== 'production') { - if (props.paginationMode === 'server' && props.rowCount == null) { - noRowCountInServerMode(); - } - } - }, [props.rowCount, props.paginationMode]); - React.useEffect(() => { if (props.paginationMode === 'client') { apiRef.current.setRowCount(visibleTopLevelRowCount); diff --git a/packages/x-data-grid/src/internals/utils/propValidation.ts b/packages/x-data-grid/src/internals/utils/propValidation.ts index 46e68732289c..ee4d70e09949 100644 --- a/packages/x-data-grid/src/internals/utils/propValidation.ts +++ b/packages/x-data-grid/src/internals/utils/propValidation.ts @@ -19,15 +19,26 @@ export const propValidatorsDataGrid: PropValidator[] = [ (props.paginationMode === 'client' && props.paginationMeta != null && [ - 'MUI X: `paginationMeta` is not used when `paginationMode` is `client`.', - 'Consider removing the `paginationMeta` prop or switching to `server` mode if your pagination is on server.', + 'MUI X: Usage of the `paginationMeta` prop with client-side pagination (`paginationMode="client"`) has no effect.', + '`paginationMeta` is only meant to be used with `paginationMode="server"`.', ].join('\n')) || undefined, (props) => (props.signature === GridSignature.DataGrid && props.paginationMode === 'client' && 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"`.') || + [ + '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"`.', + ].join('\n')) || + undefined, + (props) => + (props.paginationMode === 'server' && + props.rowCount == null && + [ + "MUI X: The `rowCount` prop must be passed using `paginationMode='server'`", + 'For more detail, see http://mui.com/components/data-grid/pagination/#index-based-pagination', + ].join('\n')) || undefined, ]; From ec4340b690c0b7dc6f4d3bbdde056fc54250ee94 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 5 Apr 2024 16:03:05 +0500 Subject: [PATCH 18/27] Simplify the effects --- .../features/pagination/useGridRowCount.ts | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts index cc41ac93eacd..17a09954bd78 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts @@ -151,35 +151,17 @@ export const useGridRowCount = ( React.useEffect(() => { if (props.paginationMode === 'client') { apiRef.current.setRowCount(visibleTopLevelRowCount); - return; - } - - if (isNumber(props.rowCount) && prevRowCountProp.current !== props.rowCount) { - prevRowCountProp.current = props.rowCount; + } else if (props.rowCount != null) { apiRef.current.setRowCount(props.rowCount); - return; } + }, [apiRef, props.paginationMode, visibleTopLevelRowCount, props.rowCount]); - const isLastPage = paginationMeta.hasNextPage === false; - if ( - isLastPage && - (rowCountState === -1 || rowCountState % paginationModel.pageSize === 0) && - visibleTopLevelRowCount < paginationModel.pageSize - ) { - // Actual count of the rows is less than the page size, reflect the value + const isLastPage = paginationMeta.hasNextPage === false; + React.useEffect(() => { + if (isLastPage && rowCountState === -1) { apiRef.current.setRowCount( - (rowCountState !== -1 - ? rowCountState - paginationModel.pageSize - : paginationModel.pageSize * paginationModel.page) + visibleTopLevelRowCount, + paginationModel.pageSize * paginationModel.page + visibleTopLevelRowCount, ); } - }, [ - apiRef, - visibleTopLevelRowCount, - props.paginationMode, - props.rowCount, - paginationMeta.hasNextPage, - paginationModel, - rowCountState, - ]); + }, [apiRef, visibleTopLevelRowCount, isLastPage, rowCountState, paginationModel]); }; From ac6671410a7c2b1af542e94e66e6a9756fb19a1b Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 5 Apr 2024 16:03:44 +0500 Subject: [PATCH 19/27] lint --- .../src/hooks/features/pagination/gridPaginationUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts index 225d75a88539..bf090fa35eb1 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts @@ -1,5 +1,4 @@ import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { buildWarning } from '../../../utils/warning'; import { GridSignature } from '../../utils'; const MAX_PAGE_SIZE = 100; From cbe16b019d62e072566c9785de375d9b8a2fc50b Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 5 Apr 2024 16:30:03 +0500 Subject: [PATCH 20/27] Lint + docs minor update --- docs/data/data-grid/pagination/pagination.md | 12 +++++++----- .../src/hooks/features/pagination/useGridRowCount.ts | 2 -- .../src/tests/pagination.DataGrid.test.tsx | 9 ++++++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index 8ca982048a78..18ba8235167d 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -169,10 +169,6 @@ const rowCount = React.useMemo(() => { Pass the props to the Grid as explained in the table above to handle the case when the actual row count is not initially available. -:::warning -The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value to a finite value. -::: - The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook resulting in unwanted computations, one possible solution could be to memoize the `paginationMeta`: ```tsx @@ -205,12 +201,18 @@ There could be two possibilities after providing the initial props (as mentioned 1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a positive value. 2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage='false'` to know that the last page is fetched and try to set a value like in the unknown row count use case. -The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Set Row Count" button. +The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. + +The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Set Row Count" button. {{"demo": "ServerPaginationGridEstimated.js", "bg": "inline"}} +:::warning +The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value to a finite value. ::: +:::info + 🌍 **Localization of the estimated row count** The Data Grid uses the [Table Pagination](/material-ui/api/table-pagination/) component from the Material UI library which doesn't support `estimated` row count. Until this is supported natively by the Table Pagination component, a workaround to make the localization work is to provide the `labelDisplayedRows` function to the `localeText.MuiTablePagination` property as per the locale you are interested in. diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts index 17a09954bd78..90c8accd71ea 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts @@ -1,6 +1,5 @@ import * as React from 'react'; import useLazyRef from '@mui/utils/useLazyRef'; -import { isNumber } from '../../../utils/utils'; import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { GridPaginationRowCountApi, GridPaginationState } from './gridPaginationInterfaces'; @@ -32,7 +31,6 @@ export const useGridRowCount = ( const paginationMeta = useGridSelector(apiRef, gridPaginationMetaSelector); const paginationModel = useGridSelector(apiRef, gridPaginationModelSelector); const previousPageSize = useLazyRef(() => gridPaginationModelSelector(apiRef).pageSize); - const prevRowCountProp = React.useRef(props.rowCount); apiRef.current.registerControlState({ stateId: 'paginationRowCount', diff --git a/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx b/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx index 0eb4721eefa0..d2ba0c6e840e 100644 --- a/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx @@ -744,8 +744,11 @@ describe(' - Pagination', () => { it('should log an error if rowCount is used with client-side pagination', () => { expect(() => { render(); - }).toErrorDev([ - '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"`.', - ]); + }).toErrorDev( + [ + '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"`.', + ].join('\n'), + ); }); }); From a234af288ed115d3e5d557308d1dfb52c067050c Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 11 Apr 2024 21:09:00 +0500 Subject: [PATCH 21/27] Update demos --- .../ServerPaginationGridEstimated.js | 25 +++++++--------- .../ServerPaginationGridEstimated.tsx | 30 +++++++------------ .../ServerPaginationGridEstimated.tsx.preview | 2 +- .../ServerPaginationGridNoRowCount.js | 13 +------- .../ServerPaginationGridNoRowCount.tsx | 23 ++------------ ...ServerPaginationGridNoRowCount.tsx.preview | 2 +- 6 files changed, 26 insertions(+), 69 deletions(-) diff --git a/docs/data/data-grid/pagination/ServerPaginationGridEstimated.js b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.js index 179b4bd6bebe..08a4e438866d 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGridEstimated.js +++ b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.js @@ -23,23 +23,18 @@ export default function ServerPaginationGridEstimated() { pageInfo: { hasNextPage }, } = useQuery(paginationModel); - const [paginationMeta, setPaginationMeta] = React.useState({}); - - React.useEffect(() => { - if (hasNextPage !== undefined) { - setPaginationMeta((prev) => { - if (prev.hasNextPage !== hasNextPage) { - return { ...prev, hasNextPage }; - } - return prev; - }); + const paginationMetaRef = React.useRef({}); + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; } + return paginationMetaRef.current; }, [hasNextPage]); - const handlePaginationModelChange = React.useCallback((newPaginationModel) => { - setPaginationModel(newPaginationModel); - }, []); - return (
@@ -55,7 +50,7 @@ export default function ServerPaginationGridEstimated() { pageSizeOptions={[10, 25, 50, 100]} paginationModel={paginationModel} paginationMode="server" - onPaginationModelChange={handlePaginationModelChange} + onPaginationModelChange={setPaginationModel} />
diff --git a/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx index 3831287b75e1..6191eb8232a8 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx +++ b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import Button from '@mui/material/Button'; import { DataGrid, useGridApiRef } from '@mui/x-data-grid'; -import type { GridPaginationMeta, GridPaginationModel } from '@mui/x-data-grid'; +import type { GridPaginationMeta } from '@mui/x-data-grid'; import { createFakeServer } from '@mui/x-data-grid-generator'; const SERVER_OPTIONS = { @@ -23,26 +23,18 @@ export default function ServerPaginationGridEstimated() { pageInfo: { hasNextPage }, } = useQuery(paginationModel); - const [paginationMeta, setPaginationMeta] = React.useState({}); - - React.useEffect(() => { - if (hasNextPage !== undefined) { - setPaginationMeta((prev) => { - if (prev.hasNextPage !== hasNextPage) { - return { ...prev, hasNextPage }; - } - return prev; - }); + const paginationMetaRef = React.useRef({}); + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; } + return paginationMetaRef.current; }, [hasNextPage]); - const handlePaginationModelChange = React.useCallback( - (newPaginationModel: GridPaginationModel) => { - setPaginationModel(newPaginationModel); - }, - [], - ); - return (
@@ -58,7 +50,7 @@ export default function ServerPaginationGridEstimated() { pageSizeOptions={[10, 25, 50, 100]} paginationModel={paginationModel} paginationMode="server" - onPaginationModelChange={handlePaginationModelChange} + onPaginationModelChange={setPaginationModel} />
diff --git a/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx.preview b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx.preview index 7ffe4eb0648b..82b7e4ec6ffe 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx.preview +++ b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx.preview @@ -11,6 +11,6 @@ pageSizeOptions={[10, 25, 50, 100]} paginationModel={paginationModel} paginationMode="server" - onPaginationModelChange={handlePaginationModelChange} + onPaginationModelChange={setPaginationModel} />
\ No newline at end of file diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js index 56cdf70c5c25..2de106edc5d3 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js @@ -36,17 +36,6 @@ export default function ServerPaginationGridNoRowCount() { return paginationMetaRef.current; }, [hasNextPage]); - const handlePaginationModelChange = React.useCallback((newPaginationModel) => { - setPaginationModel(newPaginationModel); - }, []); - - React.useEffect(() => { - if (paginationMeta?.hasNextPage === false) { - // On last page set the row count to the number of rows - apiRef.current.setRowCount(rowLength); - } - }, [apiRef, paginationMeta]); - return (
); diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx index b0118254570e..bb35ce14cebd 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx @@ -1,10 +1,5 @@ import * as React from 'react'; -import { - DataGrid, - GridPaginationMeta, - GridPaginationModel, - useGridApiRef, -} from '@mui/x-data-grid'; +import { DataGrid, GridPaginationMeta, useGridApiRef } from '@mui/x-data-grid'; import { createFakeServer } from '@mui/x-data-grid-generator'; const SERVER_OPTIONS = { @@ -41,20 +36,6 @@ export default function ServerPaginationGridNoRowCount() { return paginationMetaRef.current; }, [hasNextPage]); - const handlePaginationModelChange = React.useCallback( - (newPaginationModel: GridPaginationModel) => { - setPaginationModel(newPaginationModel); - }, - [], - ); - - React.useEffect(() => { - if (paginationMeta?.hasNextPage === false) { - // On last page set the row count to the number of rows - apiRef.current.setRowCount(rowLength); - } - }, [apiRef, paginationMeta]); - return (
); diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview index 933c72e05237..5c6f399b3aa0 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview @@ -8,5 +8,5 @@ pageSizeOptions={[5, 10, 25, 50]} paginationModel={paginationModel} paginationMode="server" - onPaginationModelChange={handlePaginationModelChange} + onPaginationModelChange={setPaginationModel} /> \ No newline at end of file From e1addb56721df807a843cc5a121e955ddec9b130 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 11 Apr 2024 21:31:02 +0500 Subject: [PATCH 22/27] Remove redundant condition --- .../src/hooks/features/pagination/useGridRowCount.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts index 90c8accd71ea..39f950032ade 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts @@ -130,11 +130,6 @@ export const useGridRowCount = ( if (rowCountState === -1) { // Row count unknown and page size changed, reset the page apiRef.current.setPage(0); - return; - } - const lastPage = Math.max(0, Math.ceil(rowCountState / model.pageSize) - 1); - if (model.page > lastPage) { - apiRef.current.setPage(lastPage); } } }, From 253791cbc4fd8d39057bf6feaa2bcb662fa563f3 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 15 Apr 2024 23:35:00 +0500 Subject: [PATCH 23/27] Apply suggestions from code review Co-authored-by: Andrew Cherniavskii Signed-off-by: Bilal Shafi --- docs/data/data-grid/pagination/pagination.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index 18ba8235167d..edad1e3e0b0d 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -136,19 +136,22 @@ This prop is ignored when the `paginationMode` is set to `client`, i.e. when the You can achieve each of these use-cases by setting the values of `rowCount`, `paginationMeta.hasNextPage`, and `estimatedRowCount` props. The following table summarizes the use of these props in each of these scenarios. -| | Known row count | Unknown row count | Estimated row count | -| :--------------------------- | :-------------- | :---------------- | :------------------ | -| `rowCount` | `number` | `-1` | `-1` | -| `paginationMeta.hasNextPage` | — | `boolean` | `boolean` | -| `estimatedRowCount` | — | — | `number` | +| | `rowCount` | `paginationMeta.hasNextPage` | `estimatedRowCount` | +| :-------------------- | :--------- | :--------------------------- | :------------------ | +| Known row count | `number` | — | — | +| Unknown row count | `-1` | `boolean` | — | +| Estimated row count | `-1` | `boolean` | `number` | The following examples demonstrate each of these scenarios: #### Known row count -The value of the `rowCount` might become `undefined` during loading if it's handled by some external fetching hook resulting in reseting the page to zero. -In such situations, memoizing the `rowCount` value could overcome this issue. +{{"demo": "ServerPaginationGrid.js", "bg": "inline"}} + +:::warning +If the value `rowCount` becomes `undefined` during loading, it will reset the page to zero. +To avoid this issue, you can memoize the `rowCount` value to ensure it doesn't change during loading: ```jsx const rowCountRef = React.useRef(pageInfo?.totalRowCount || 0); @@ -163,7 +166,7 @@ const rowCount = React.useMemo(() => { ; ``` -{{"demo": "ServerPaginationGrid.js", "bg": "inline"}} +::: #### Unknown row count From 26fbd67e2dac7e96138aeaeae59766b54ff593a4 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 15 Apr 2024 23:43:51 +0500 Subject: [PATCH 24/27] Update docs --- docs/data/data-grid/pagination/pagination.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index edad1e3e0b0d..f8ca19278f95 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -136,17 +136,16 @@ This prop is ignored when the `paginationMode` is set to `client`, i.e. when the You can achieve each of these use-cases by setting the values of `rowCount`, `paginationMeta.hasNextPage`, and `estimatedRowCount` props. The following table summarizes the use of these props in each of these scenarios. -| | `rowCount` | `paginationMeta.hasNextPage` | `estimatedRowCount` | -| :-------------------- | :--------- | :--------------------------- | :------------------ | -| Known row count | `number` | — | — | -| Unknown row count | `-1` | `boolean` | — | -| Estimated row count | `-1` | `boolean` | `number` | +| | `rowCount` | `paginationMeta.hasNextPage` | `estimatedRowCount` | +| :------------------ | :--------- | :--------------------------- | :------------------ | +| Known row count | `number` | — | — | +| Unknown row count | `-1` | `boolean` | — | +| Estimated row count | `-1` | `boolean` | `number` | The following examples demonstrate each of these scenarios: #### Known row count - {{"demo": "ServerPaginationGrid.js", "bg": "inline"}} :::warning @@ -170,8 +169,11 @@ const rowCount = React.useMemo(() => { #### Unknown row count -Pass the props to the Grid as explained in the table above to handle the case when the actual row count is not initially available. +Pass the props to the Grid as explained in the table above to handle the case when the actual row count is not initially available as the following example demonstrates. + +{{"demo": "ServerPaginationGridNoRowCount.js", "bg": "inline"}} +:::warning The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook resulting in unwanted computations, one possible solution could be to memoize the `paginationMeta`: ```tsx @@ -188,7 +190,7 @@ const paginationMeta = React.useMemo(() => { }, [hasNextPage]); ``` -{{"demo": "ServerPaginationGridNoRowCount.js", "bg": "inline"}} +::: #### Estimated row count From 0a0ef75b4c88da6b2246390f882c291627583bc5 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 17 Apr 2024 14:48:36 +0500 Subject: [PATCH 25/27] A few updates here and there --- docs/data/data-grid/pagination/pagination.md | 22 +++++++++---------- .../src/components/GridPagination.tsx | 4 ++-- .../pagination/gridPaginationUtils.ts | 3 ++- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index f8ca19278f95..42440e38c157 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -146,6 +146,8 @@ The following examples demonstrate each of these scenarios: #### Known row count +Pass the props to the Grid as explained in the table above to handle the case when the actual row count is known, as the following example demonstrates. + {{"demo": "ServerPaginationGrid.js", "bg": "inline"}} :::warning @@ -169,7 +171,7 @@ const rowCount = React.useMemo(() => { #### Unknown row count -Pass the props to the Grid as explained in the table above to handle the case when the actual row count is not initially available as the following example demonstrates. +Pass the props to the Grid as explained in the table above to handle the case when the actual row count is unknown, as the following example demonstrates. {{"demo": "ServerPaginationGridNoRowCount.js", "bg": "inline"}} @@ -194,26 +196,22 @@ const paginationMeta = React.useMemo(() => { #### Estimated row count -There could be possibilities when the accurate row count is not initially available for many reasons such as: - -1. For some databases, computing `rowCount` upfront is a costly operation due to the scale of data or how the data is structured. -2. Some data structures don't have the `rowCount` information until the very last page. +Estimated row count could be considered a hybrid approach of known and unknown row count cases, where the actual row count is not initially available, but an estimated row count is available. -In such scenarios, some backends provide an estimated row count initially which could be used to estimate the number of pages until the actual row count is available. In some cases, this estimate value could also be more than the actual row count. Therefore, the Grid uses it only to show the user an estimated number of rows, and uses `paginationMeta.hasNextPage` as a single source of truth to check whether more records are available on the server. +Initially, when an `estimatedRowCount` is set and `rowCount='-1'`, the grid work identical to the "Unknown row count" use case, with the exception that the `estimatedRowCount` is used to show more detailed information in the pagination footer. -There could be two possibilities after providing the initial props (as mentioned in the table above): +Once the current row count exceeds the `estimatedRowCount`, the Grid will ignore the `estimatedRowCount` and completely work as the "Unknown row count" use case. -1. The actual row count is fetched from the server lazily and provided to the Grid using the `rowCount` prop. The `estimatedRowCount` prop will be ignored once the `rowCount` prop is set to a positive value. -2. The user has already reached the estimated last page and the actual row count is still not available, in that case, the Grid could take the help of the `hasNextPage='false'` to know that the last page is fetched and try to set a value like in the unknown row count use case. +Meanwhile, if the `rowCount` is set to a valid value (either by updating the `rowCount` prop or setting `hasNextPage='false'`), the Grid will shift to the "Known row count" use case. -The following example demonstrates the use of the `estimatedRowCount` and `hasNextPage` props to handle the case when the actual row count is not initially available. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. - -The Grid keeps fetching the next page until `hasNextPage` is `false` or the actual row count is provided to the Grid lazily, you can do that by clicking the "Set Row Count" button. +The following example demonstrates this scenario. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. You can set the `rowCount` value by hitting the "Set Row Count" button. {{"demo": "ServerPaginationGridEstimated.js", "bg": "inline"}} :::warning The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value to a finite value. + +If an external data fetching library sets the values to undefined during loading, you can memoize the `paginationMeta` value to ensure it doesn't change during loading as shown in the "Unknown row count" section. ::: :::info diff --git a/packages/x-data-grid/src/components/GridPagination.tsx b/packages/x-data-grid/src/components/GridPagination.tsx index 6a97e3ac810f..8a5c5fd0e763 100644 --- a/packages/x-data-grid/src/components/GridPagination.tsx +++ b/packages/x-data-grid/src/components/GridPagination.tsx @@ -74,7 +74,7 @@ const GridPagination = React.forwardRef< const { paginationMode, loading, estimatedRowCount } = rootProps; const computedProps: Partial = React.useMemo(() => { - if (paginationMode === 'server' && loading) { + if (rowCount === -1 && paginationMode === 'server' && loading) { return { backIconButtonProps: { disabled: true }, nextIconButtonProps: { disabled: true }, @@ -82,7 +82,7 @@ const GridPagination = React.forwardRef< } return {}; - }, [loading, paginationMode]); + }, [loading, paginationMode, rowCount]); const lastPage = React.useMemo(() => Math.max(0, pageCount - 1), [pageCount]); diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts index bf090fa35eb1..61c43af8df96 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts @@ -11,7 +11,8 @@ export const getPageCount = (rowCount: number, pageSize: number, page: number): } if (rowCount === -1) { - return page + 1; + // With unknown row-count, we can assume a page after the current one + return page + 2; } return 0; From ad26ba18fe1a374772370d17a5f7bdd0511ab1f4 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 18 Apr 2024 00:08:48 +0500 Subject: [PATCH 26/27] Apply suggestions from code review Co-authored-by: Andrew Cherniavskii Signed-off-by: Bilal Shafi --- docs/data/data-grid/pagination/pagination.md | 23 ++++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index 42440e38c157..5933a565e49b 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -107,7 +107,7 @@ Check out [Selection—Usage with server-side pagination](/x/react-data-grid/row ### Index-based pagination -The index-based pagination is the most common type of pagination. It follows the same pattern as the client-side pagination (page, pageSize), but the data is fetched from the server. +The index-based pagination uses the `page` and `pageSize` to fetch the data from the server page by page. To enable server-side pagination, you need to: @@ -116,7 +116,7 @@ To enable server-side pagination, you need to: The server-side pagination can be further categorized into sub-types based on the availability of the total number of rows or `rowCount`. -The Grid uses the `rowCount` to calculate the number of pages and to show the information about the current state of the pagination in the footer. +The Data Grid uses the `rowCount` to calculate the number of pages and to show the information about the current state of the pagination in the footer. You can provide the `rowCount` in one of the following ways: - Initialize - use `initialState.pagination.rowCount` prop to initialize the `rowCount` @@ -134,7 +134,7 @@ The `rowCount` prop is used in server-side pagination mode to inform the DataGri This prop is ignored when the `paginationMode` is set to `client`, i.e. when the pagination is handled on the client-side. ::: -You can achieve each of these use-cases by setting the values of `rowCount`, `paginationMeta.hasNextPage`, and `estimatedRowCount` props. The following table summarizes the use of these props in each of these scenarios. +You can configure `rowCount`, `paginationMeta.hasNextPage`, and `estimatedRowCount` props to handle the above scenarios. | | `rowCount` | `paginationMeta.hasNextPage` | `estimatedRowCount` | | :------------------ | :--------- | :--------------------------- | :------------------ | @@ -142,11 +142,9 @@ You can achieve each of these use-cases by setting the values of `rowCount`, `pa | Unknown row count | `-1` | `boolean` | — | | Estimated row count | `-1` | `boolean` | `number` | -The following examples demonstrate each of these scenarios: - #### Known row count -Pass the props to the Grid as explained in the table above to handle the case when the actual row count is known, as the following example demonstrates. +Pass the props to the Data Grid as explained in the table above to handle the case when the actual row count is known, as the following example demonstrates. {{"demo": "ServerPaginationGrid.js", "bg": "inline"}} @@ -171,7 +169,7 @@ const rowCount = React.useMemo(() => { #### Unknown row count -Pass the props to the Grid as explained in the table above to handle the case when the actual row count is unknown, as the following example demonstrates. +Pass the props to the Data Grid as explained in the table above to handle the case when the actual row count is unknown, as the following example demonstrates. {{"demo": "ServerPaginationGridNoRowCount.js", "bg": "inline"}} @@ -196,15 +194,16 @@ const paginationMeta = React.useMemo(() => { #### Estimated row count -Estimated row count could be considered a hybrid approach of known and unknown row count cases, where the actual row count is not initially available, but an estimated row count is available. +Estimated row count could be considered a hybrid approach that switches between the "Known row count" and "Unknown row count" use cases. -Initially, when an `estimatedRowCount` is set and `rowCount='-1'`, the grid work identical to the "Unknown row count" use case, with the exception that the `estimatedRowCount` is used to show more detailed information in the pagination footer. +Initially, when an `estimatedRowCount` is set and `rowCount={-1}`, the Data Grid behaves as in the "Unknown row count" use case, but with the `estimatedRowCount` value shown in the pagination footer. -Once the current row count exceeds the `estimatedRowCount`, the Grid will ignore the `estimatedRowCount` and completely work as the "Unknown row count" use case. +If the number of rows loaded exceeds the `estimatedRowCount`, the Data Grid ignores the `estimatedRowCount` and the behavior is identical to the "Unknown row count" use case. -Meanwhile, if the `rowCount` is set to a valid value (either by updating the `rowCount` prop or setting `hasNextPage='false'`), the Grid will shift to the "Known row count" use case. +When the `hasNextPage` returns `false` or `rowCount` is set to a positive number, the Data Grid switches to the "Known row count" behavior. -The following example demonstrates this scenario. The actual row count is `1000` but the Grid is initially provided with an estimated row count of `100`. You can set the `rowCount` value by hitting the "Set Row Count" button. +In the following example, the actual row count is `1000` but the Data Grid is initially provided with `estimatedRowCount={100}`. +You can set the `rowCount` to the actual row count by pressing the "Set Row Count" button. {{"demo": "ServerPaginationGridEstimated.js", "bg": "inline"}} From 2f8f60190e391c3fa1cc1c8dda0202f229dc36c5 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 18 Apr 2024 00:15:38 +0500 Subject: [PATCH 27/27] Update the docs --- docs/data/data-grid/pagination/pagination.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index 5933a565e49b..d23efc50aa1f 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -119,9 +119,12 @@ The server-side pagination can be further categorized into sub-types based on th The Data Grid uses the `rowCount` to calculate the number of pages and to show the information about the current state of the pagination in the footer. You can provide the `rowCount` in one of the following ways: -- Initialize - use `initialState.pagination.rowCount` prop to initialize the `rowCount` -- Control - use the `rowCount` prop along with `onRowCountChange` to control the `rowCount` and reflect the changes when the row count is updated -- Set using the `apiRef` - use the `apiRef.current.setRowCount` method to set the `rowCount` after the Grid is initialized +- **Initialize.** + Use the `initialState.pagination.rowCount` prop to initialize the `rowCount`. +- **Control.** + Use the `rowCount` prop along with `onRowCountChange` to control the `rowCount` and reflect the changes when the row count is updated. +- **Set using the API.** + Use the `apiRef.current.setRowCount` method to set the `rowCount` after the Grid is initialized. There can be three different possibilities regarding the availability of the `rowCount` on the server-side: