Summary answer
The internal grid state would get out of sync with the rows prop. I'm sure there is a way to solve this otherwise, based on what the docs hint at what can be an issue, I went for the imperative use of the apiRef to update the rows and subsequently force an update.
Notes:
There may be some redundant code
- using the
apiRef directly may not be required given I also created a new side-effect that tracks changes in api status && a "read-once" latch
- calling both the
apiRef to update the rows and then force an update is likely redundant; I suspect the forceUpdate is called internally following the call to update the rows
the dynamic sizing is not required, but is an extra layer of functionality that highlights how the grid controls the number of rows in view (size of the "viewport" given a rowHeight setting).
What worked
There are two pieces to this: the first is setting-up how many rows to display. The second, is making sure the internal state of the grid is in fact being rendered. Prior to this solution, the extra scrolling was consistent with the "internal" state of the grid, not what was being rendered (the render was "behind" by a cycle).
The DataGrid size is set by wrapping the component in a component with a fixed size. In the docs they use a div. I compute the gridStyle.height using a derivative of MAX_ROWS and ROW_HEIGHT settings.
// max size is 9 rows before I start scrolling
const gridHeight = (maxRecords, rowHeight, header, footer, adjustment) => {
const gridBody = Math.min(9, Math.max(2, maxRecords)) * rowHeight;
return gridBody + header + footer + adjustment;
};
The grid wrapped in the div that uses the computed height:
<div style={gridStyle}>
<DataGrid
className={clsx('My-ValueGrid', type)}
apiRef={apiRef}
...
/>
</div>
The scroll event handler sets the state readyForMore. This flag is the equivalent of a "latch" that ensures a single (not infinite) re-render when the requested data status === 'success'
const handleOnRowScrollEnd = ({ viewportPageSize }) => {
if (apiRef.current.getVisibleRowModels().size < MAX_ROWS) {
const nextPageSize = Math.max(PAGE_SIZE, viewportPageSize)
fetchPage({ pageSize: nextPageSize, after: max });
setReadyForMore(true); // <<< open/reset the latch
}
};
The state flag readyForMore is then a trigger in the components main effect:
useEffect(() => {
if (status === 'pending') {
/* do nothing */
}
if (status === 'error') {
setError({ message: JSON.stringify(data.cache) });
}
if (status === 'success' && readyForMore) {
const { rows: newRows, selectedRows: newSelected } = toGridValues(
data.cache,
storeSelectionModel,
);
setSelectionModel([...selectionModel, ...newSelected]);
apiRef.current.updateRows(newRows); // <<< going imperative...
apiRef.current.forceUpdate(); // <<< may not be required
setReadyForMore(false); // <<< close the latch (prevent infinite renders)
setMaxRows(data.cache.totalCount); // <<< only changes when changing filters
}
}, [
apiRef,
data.cache,
readyForMore,
selectionModel,
status,
storeSelectionModel,
]);