Hello everyone,
First of all, thank you @fabjiro. @fabjiro's answer was quite effective in helping me solve my problem. Using @fabjiro's solution, I further improved it to better suit my own project.
If there's any issue regarding the code blocks I've shared, please feel free to ask or point it out. Wishing everyone good work!
// File Name => ListDataGridPage.tsx
import React from 'react';
import { useLazyGetEmployeesQuery } from '../../../../../redux/slices/services/introductionApiSlices';
import { employeeColumns, EmployeeRowType, ListDataGridRef } from './listDataGridPageTypes';
import ListDataGrid from '../../../../../components/introduction/dataGrid/listDataGrid/ListDataGrid';
import BoxComp from '../../../../../components/base/box/Box';
const ListDataGridPage: React.FC = () => {
const [triggerGetEmployees] = useLazyGetEmployeesQuery();
const listDataGridRef = React.useRef<ListDataGridRef>(null);
// States for infinite scroll implementation
const [rows, setRows] = React.useState<EmployeeRowType[]>([]); // Stores all loaded rows
const [skipCount, setSkipCount] = React.useState(0); // Tracks the number of items to skip
const [loading, setLoading] = React.useState(false); // Prevents multiple simultaneous data fetches
// Function to load more data when scrolling
const loadData = async () => {
if (!loading) {
try {
setLoading(true);
const { data } = await triggerGetEmployees({
maxResultCount: '40', // Number of items to fetch per request
skipCount: skipCount.toString(), // Offset for pagination
});
if (data) {
if (Array.isArray(data.data.items)) {
// Append new items to existing rows
setRows((prev) => [...prev, ...data.data.items]);
// Increment skip count for next fetch
setSkipCount((prev) => prev + 40);
} else {
console.error('Invalid data format: items is not an array', data);
}
}
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
}
};
// Load initial data on component mount
React.useEffect(() => {
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<BoxComp sx={{ height: 500, width: '100%' }}>
<ListDataGrid
ref={listDataGridRef}
rows={rows}
columns={employeeColumns}
onNextPage={loadData}
isLoading={loading}
threshold={5} // Percentage threshold to trigger next page load
/>
</BoxComp>
);
};
export default ListDataGridPage;
// File Name => ListDataGrid.tsx
import React from 'react';
import useLanguageContext from '../../../../hooks/useLanguageContext';
import { ListDataGridProps, ListDataGridRef } from './listDataGridTypes';
import { listDataGridPropsPrepareColumn } from './listDataGridMethods';
import DataGridComp from '../../../base/dataGrid/DataGrid';
import { useGridApiRef } from '@mui/x-data-grid';
const ListDataGrid = React.forwardRef<ListDataGridRef, ListDataGridProps>((props, ref) => {
const { columns, rows, onNextPage, isLoading = false, threshold = 0 } = props;
const { translate } = useLanguageContext();
const apiRef = useGridApiRef();
// Refs for managing scroll behavior
const scrollMonitor = React.useRef<() => void>(); // Tracks scroll event subscription
const isInitialMount = React.useRef(true); // Prevents initial trigger
const isRequestLocked = React.useRef(false); // Prevents multiple simultaneous requests
// Handle scroll events and trigger data loading when needed
const handleScroll = React.useCallback(() => {
// Skip if a request is already in progress
if (isRequestLocked.current) {
return;
}
// Skip the first scroll event after mount
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
if (apiRef.current?.instanceId) {
const elementScroll = apiRef.current.rootElementRef.current?.children[0].children[1];
if (elementScroll) {
// Calculate scroll positions and threshold
const maxScrollTop = elementScroll.scrollHeight - elementScroll.clientHeight;
const scrollPosition = apiRef.current.getScrollPosition();
const scrollThreshold = maxScrollTop * (1 - threshold / 100);
// Check if we've scrolled past the threshold
if (scrollPosition.top >= scrollThreshold) {
// Lock requests to prevent multiple triggers
isRequestLocked.current = true;
// Trigger the next page load
onNextPage?.();
// Release the lock after a delay
setTimeout(() => {
isRequestLocked.current = false;
}, 1000);
}
}
}
}, [apiRef, threshold, onNextPage]);
// Set up scroll event listener
React.useEffect(() => {
if (apiRef.current?.instanceId) {
// Subscribe to grid's scroll position changes
scrollMonitor.current = apiRef.current.subscribeEvent('scrollPositionChange', () => {
handleScroll();
});
}
// Cleanup scroll event listener on unmount
return () => {
if (scrollMonitor.current) {
scrollMonitor.current();
}
};
}, [apiRef, handleScroll]);
const preparedColumns = React.useMemo(() => {
const preparedCols = columns.map((column) => ({
...listDataGridPropsPrepareColumn(column),
headerName: column.isTranslation === false ? column.headerName : translate(column.headerName as string),
}));
return preparedCols;
}, [columns, translate]);
React.useImperativeHandle(ref, () => ({
getDataGrid: () => apiRef.current,
}));
return (
<DataGridComp
apiRef={apiRef}
columns={preparedColumns}
rows={rows}
showCellVerticalBorder={true}
showColumnVerticalBorder={true}
hideFooter={true}
hideFooterPagination={true}
hideFooterSelectedRowCount={true}
loading={isLoading}
/>
);
});
ListDataGrid.displayName = 'ListDataGrid';
export default React.memo(ListDataGrid);
// File Name => DataGrid.tsx
import useLanguageContext from '../../../hooks/useLanguageContext';
import { getLocaleText } from '../../../utils/locale/dataGridLocales';
import { Language } from '../../../utils/enums/languages';
import { DataGridCompProps, dataGridCompDefaultProps } from './dataGridHelper';
import { DataGrid } from '@mui/x-data-grid';
const DataGridComp = (props: DataGridCompProps) => {
const { ...dataGridProps } = { ...dataGridCompDefaultProps, ...props };
const { language } = useLanguageContext();
return <DataGrid {...dataGridProps} localeText={getLocaleText(language as Language)} />;
};
DataGridComp.displayName = 'DataGridComp';
export default DataGridComp;
// File Name => listDataGridTypes.ts
import { GridApi } from '@mui/x-data-grid';
import { DataGridCompColDef, DataGridCompValidRowModel } from '../../../base/dataGrid/dataGridHelper';
export interface ListDataGridRef {
getDataGrid: () => GridApi | null;
}
export interface ListDataGridProps {
columns: DataGridCompColDef[];
rows: DataGridCompValidRowModel[];
// Function triggered when more data needs to be loaded
onNextPage?: () => void;
// Indicates whether data is currently being fetched
isLoading?: boolean;
// Percentage of scroll progress at which to trigger next page load (0-100)
threshold?: number;
}