79558864

Date: 2025-04-06 23:58:41
Score: 0.5
Natty:
Report link

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!

TSX Files

// 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;

TS Files

// 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;
}
Reasons:
  • Blacklisted phrase (0.5): thank you
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @fabjiro
  • User mentioned (0): @fabjiro's
  • User mentioned (0): @fabjiro's
  • Low reputation (0.5):
Posted by: Mümin ZEHİR