0

I'm currently working on a complex React table component which is giving me some performance and usability issues. The primary concern is that, when selecting rows, the scroll position of the table jumps, creating a disorienting experience for the user. I'd love to hear your insights and suggestions on how to optimize this component and fix the stated issue.

Component Overview:

The table component (TableComponent) I'm developing leverages the react-base-table package. Here are some of its functionalities and features:

Dynamic Row Selection: Users can select rows with a simple click or multiple rows using the shift key. Selections are visually highlighted, and the list of selected rows can be manipulated outside of this table. They are also manipulated outside of this table using the SelectedRowsSummary component to save or edit sequences.

Filtering: A filtering dialog allows users to apply filters to specific columns. Active filters italicize column headers.

Column Reordering: A separate dialog lets users reorder columns.

Sorting: Columns are sortable, and the component keeps track of the current sort states . State Restoration: The component can restore its state, including applied filters and sort order, from local storage.

Technical Challenges:

Scroll Position Jump: When rows are selected, the scroll position of the table shifts unexpectedly. I've attempted to store the scroll position in a ref and then restore it, but this isn't a smooth solution.

Mounting & Unmounting: Due to the dynamic nature of the table, and the way React works, components mount and unmount frequently. This behavior results in the ref to the table sometimes being undefined when the layout effect runs. As a workaround, I'm querying the DOM directly to get the scrollable element, but I'm aware this isn't the React way of doing things.

Efficiency: The current structure and logic, especially with all the memoization and state management, may not be the most efficient. I'm concerned this might be contributing to the performance issues.

Seeking Suggestions:

I'm particularly interested in insights on:

How to maintain the scroll position smoothly after row selection.

How to efficiently manage state, especially with such a dynamic component.

Whether there's a more 'React-like' way to handle the challenges I'm facing, rather than querying the DOM directly.

Any other general optimizations or best practices that can be applied to this component.

All the code from the relevant components:

Table

    import React, { useState, useEffect, useMemo, useCallback, useRef, useLayoutEffect } from 'react';
    import Table, { AutoResizer, Column, ColumnShape } from 'react-base-table';
    
    import { Notification } from '@sms/plasma-ui';
    import moment from 'moment';
    import styled from 'styled-components';
    
    import { pssApi } from '../../services';
    import { ReorderColumnsDialog, SelectedRowsSummary, FilterDialog } from './components';
    import { isSameSlab, extractAllSlabs, getData, handleData } from './utils';
    
    import 'react-base-table/styles.css';
    import html2canvas from 'html2canvas';
    
    const TableComponent = ({
      columns,
      data,
      multiValueFilter,
      route,
    }: {
      columns: any[];
      data: any[];
      multiValueFilter: any;
      route: any;
    }) => {
      const [isDialogOpen, setDialogOpen] = useState(false);
      const [selectedColumn, setSelectedColumn] = useState(null);
      const [currentFilterValue, setCurrentFilterValue] = useState(undefined);
      const [selectedRowIds, setSelectedRowIds] = useState([]);
      const [selectedRowIdsOld, setSelectedRowIdsOld] = useState([]);
      const [firstRowIndex, setFirstRowIndex] = useState(null);
      const [reorderDialogOpen, setReorderDialogOpen] = useState(false);
      const [appliedFilters, setAppliedFilters] = useState>({});
      const [sortConfig, setSortConfig] = useState(null);
      const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 });
      const [dialogsState, setDialogsState] = useState>({});
      const [showSelectedOnTop, setShowSelectedOnTop] = useState(false);
      const [sequenceName, setSequenceName] = useState('');
      const [sequenceId, setSequenceId] = useState(null);
      const [startDate, setStartDate] = useState(undefined);
      const [casterType, setCasterType] = useState(null);
      const [oneCasterChecked, setOneCasterChecked] = useState(false);
      const [currentColumns, setCurrentColumns] = useState(() => getData(columns));
      const [isEdition, setIsEdition] = useState(false);
      const [dataScenario, setDataScenario] = useState(null);
      const scrollPositionRef = useRef(0);
    
      const sortStatesConstructorFunction = () => {
        return currentColumns.reduce((acc: any, column: any) => {
          if (column?.defaultSort) {
            acc[column.accessor] = column.defaultSort;
          }
          return acc;
        }, {});
      };
    
      const [sortStates, setSortStates] = useState>(sortStatesConstructorFunction);
    
      const singleSort = (data: any[], dataKey: string, order: 'asc' | 'desc') => {
        return [...data].sort((a, b) => {
          const dir = order === 'asc' ? 1 : -1;
          if (a[dataKey] > b[dataKey]) return dir;
          if (a[dataKey]  {
        setShowSelectedOnTop((prev) => !prev);
      }, []);
    
      const handleClearAllFilters = useCallback(() => {
        setAppliedFilters({});
        setSortConfig(null);
        setSortStates(sortStatesConstructorFunction);
    
        if (isEdition && sequenceId) {
          localStorage.removeItem(`sequenceTableState_${sequenceId}`);
        }
      }, [setAppliedFilters, setSortConfig, isEdition, sequenceId]);
    
      const filteredAndSortedData = useMemo(() => {
        let outputData = multiValueFilter(data, appliedFilters);
    
        if (sortConfig) {
          const { dataKey, order } = sortConfig;
          outputData = singleSort(outputData, dataKey, order);
        }
    
        if (showSelectedOnTop) {
          const selectedItems = [];
          const unselectedItems = [...outputData];
    
          for (const rowId of selectedRowIds) {
            const index = unselectedItems.findIndex((item) => item.id === rowId);
            if (index !== -1) {
              selectedItems.push(unselectedItems[index]);
              unselectedItems.splice(index, 1);
            }
          }
    
          return [...selectedItems, ...unselectedItems];
        }
    
        return outputData;
      }, [appliedFilters, data, multiValueFilter, sortConfig, selectedRowIds, showSelectedOnTop]);
    
      const setFilter = useCallback(
        (
          columnAccessor: string,
          value:
            | string
            | string[]
            | { datetime: { startDate: moment.Moment | null; endDate: moment.Moment | null } }
            | undefined,
        ) => {
          setAppliedFilters((prev) => ({
            ...prev,
            [columnAccessor]: value,
          }));
        },
        [setAppliedFilters],
      );
    
      const onColumnSort = useCallback(
        ({ column, key, order }: any) => {
          let dataKey = column.dataKey ?? column.accessor;
    
          setSortStates((prev) => {
            if (prev[key] === 'desc') {
              const newState = { ...prev };
              delete newState[key];
              return newState;
            }
            return { ...prev, [key]: order };
          });
    
          setSortConfig({ key, order, dataKey });
        },
        [setSortStates, setSortConfig],
      );
    
      const updateDialogState = useCallback(
        (accessor: string, newValues: any) => {
          setDialogsState((prev) => ({
            ...prev,
            [accessor]: {
              ...prev[accessor],
              ...newValues,
            },
          }));
        },
        [setDialogsState],
      );
    
      const toggleRowSelection = useCallback(
        async (rowId: any, index: number, shiftKey: boolean) => {
          const scrollableElement = document.querySelector('[class*=BaseTable__body]');
          if (scrollableElement) {
            scrollPositionRef.current = scrollableElement.scrollTop;
          }
    
          setSelectedRowIds((prevIds: any) => {
            const newSelectedRowIds = [...prevIds];
            if (shiftKey && firstRowIndex !== null) {
              const start = Math.min(firstRowIndex, index);
              const end = Math.max(firstRowIndex, index);
              const idsToSelect = filteredAndSortedData.slice(start, end + 1).map((row: any) => row.id);
              newSelectedRowIds.push(...idsToSelect);
            } else if (newSelectedRowIds.includes(rowId)) {
              const index = newSelectedRowIds.indexOf(rowId);
              if (index > -1) {
                newSelectedRowIds.splice(index, 1);
              }
            } else {
              setFirstRowIndex(index);
              newSelectedRowIds.push(rowId);
            }
    
            return Array.from(new Set(newSelectedRowIds));
          });
        },
        [setSelectedRowIds, firstRowIndex, filteredAndSortedData],
      );
    
      const selectedRows = useMemo(() => {
        return selectedRowIds.map((rowId) => data.find((row) => row.id === rowId)).filter(Boolean);
      }, [data, selectedRowIds]);
    
      const currentDialogState = useMemo(() => dialogsState[selectedColumn?.accessor], [
        dialogsState,
        selectedColumn?.accessor,
      ]);
    
      useEffect(() => {
        const handleEditSequence = async (id: string) => {
          try {
            const { data: sequence } = await pssApi.getSequenceById({
              params: {
                id,
              },
            });
    
            if (!sequence) {
              return;
            }
    
            setSequenceName(sequence?.name);
            setStartDate(moment(sequence?.startDate));
    
            if (sequence?.scheduledCaster) {
              setCasterType(sequence?.scheduledCaster);
              setOneCasterChecked(true);
            } else {
              setOneCasterChecked(false);
            }
    
            const { data: dataScenario } = await pssApi.getScenarioMaterialList({ params: { sequenceScenarioId: id } });
    
            const allSlabs = extractAllSlabs(dataScenario);
    
            setDataScenario(dataScenario);
    
            const newSelectedRowIds: string[] = [];
            for (let i = 0; i ;
        if (id) {
          handleEditSequence(id);
        } else {
          setSequenceName('');
          setSelectedRowIds([]);
          setSelectedRowIdsOld([]);
          setIsEdition(false);
          setSequenceId(null);
          setStartDate(undefined);
          setDataScenario(null);
          setOneCasterChecked(false);
          setCasterType(null);
        }
      }, [route]);
    
      const RowRenderer = useMemo(
        () => ({
          rowIndex,
          rowData,
          cells,
          toggleRowSelection,
          selectedRowIds,
        }: {
          cells: React.ReactNode[];
          columns: ColumnShape;
          rowData: any;
          rowIndex: number;
          toggleRowSelection: (rowId: string, index: number, shiftKey: boolean) => void;
          selectedRowIds: string[];
        }) => {
          const isSelected = selectedRowIds.includes(rowData.id);
    
          const rowStyle = {
            backgroundColor: isSelected ? '#cce6ff' : '#fff',
            fontWeight: isSelected ? 'bold' : 'normal',
            display: 'flex',
            alignItems: 'stretch',
          };
    
          const wrappedCells = cells.map((cell: any, index: number) => {cell});
    
          return (
             toggleRowSelection(rowData.id, rowIndex, event.shiftKey)}>
              {wrappedCells}
            
          );
        },
        [],
      );
    
      const CellRenderer = useMemo(
        () => ({ cellData }: { cellData: string }) => {
          const cellStyle = {
            display: 'flex',
            alignItems: 'center',
            padding: '8px',
            whiteSpace: 'nowrap',
            cursor: 'pointer',
            transition: 'background-color 0.2s',
          };
    
          return {cellData};
        },
        [],
      );
    
      const headerCellStyle = {
        fontWeight: 'bold',
        cursor: 'pointer',
        padding: '0 4px',
        alignItems: 'center',
        whiteSpace: 'nowrap',
      };
    
      const Container = styled.div`
        width: 100%;
        height: calc(100vh - 240px);
      `;
    
      const handleColumnResizeEnd = useCallback(
        ({ column, width }: { column: ColumnShape; width: number }) => {
          const newColumns = [...currentColumns];
          const columnIndex = newColumns.findIndex((col) => col.accessor === column.dataKey);
    
          if (columnIndex !== -1) {
            newColumns[columnIndex].defaultWidth = width;
    
            handleData(newColumns);
          }
        },
        [currentColumns],
      );
    
      const openFilterDialog = useCallback(
        (event: React.MouseEvent, column: ColumnShape) => {
          event.stopPropagation();
          setSelectedColumn(column);
    
          if (Object.keys(appliedFilters).length) {
            const currentFilter = appliedFilters[column.accessor];
            setCurrentFilterValue(currentFilter ? currentFilter : undefined);
          }
    
          const rect = event.currentTarget.getBoundingClientRect();
          const x = rect.right + 10;
          const y = rect.bottom + 10;
    
          const dialogWidth = 300;
          const dialogHeight = 200;
          const maxX = window.innerWidth - dialogWidth - 10;
          const maxY = window.innerHeight - dialogHeight - 10;
          const dialogX = Math.min(x, maxX);
          const dialogY = Math.min(y, maxY);
    
          setDialogPosition({ x: dialogX, y: dialogY });
          setDialogOpen(true);
        },
        [appliedFilters],
      );
    
      useLayoutEffect(() => {
        const restoreScroll = () => {
          const scrollableElement = document.querySelector('[class*=BaseTable__body]');
    
          if (scrollableElement) {
            scrollableElement.scrollTop = scrollPositionRef.current;
          }
        };
    
        const timeoutId = setTimeout(restoreScroll, 1);
    
        return () => {
          clearTimeout(timeoutId);
        };
      }, [selectedRowIds]);
    
      return (
        
          
    
          
            
              {({ width, height }) => (
                 (
                    
                  )}
                >
                  {currentColumns
                    .filter((column: any) => !column?.hidden)
                    .map((column: any, index: number) => (
                       (
                           openFilterDialog(event, column)}
                          >
                            {column.Header}
                          
                        )}
                        cellRenderer={(props: {
                          cellData: any;
                          columns: ColumnShape[];
                          column: ColumnShape;
                          columnIndex: number;
                          rowData: unknown;
                          rowIndex: number;
                          container: Table;
                        }) => }
                      />
                    ))}
                
              )}
            
          
          {isDialogOpen && (
             setDialogOpen(false)}
              setFilter={setFilter}
              position={dialogPosition}
              setCurrentFilterValue={setCurrentFilterValue}
              data={data}
              dialogState={currentDialogState || {}}
              updateDialogState={updateDialogState}
              onColumnSort={onColumnSort}
            />
          )}
          {reorderDialogOpen && (
             setReorderDialogOpen(false)}
              onUpdateColumns={setCurrentColumns}
            />
          )}
        
      );
    };
    
    export default React.memo(TableComponent);

Thank you for your time and expertise!

1
  • I could not post the SelectedRowsSummary because of stackoverflow limit of 30000 characters... Commented Sep 22, 2023 at 14:59

2 Answers 2

0

I was able to accomplish the goal by a large refactor. First I isolated the selection part in a context and using localStorage as a means of not consuming this data in the table which would trigger a rerender.

import React, { useState, FC, Dispatch, SetStateAction, useEffect } from 'react';

interface SelectionContextType {
  selectedRowIds: string[];
  toggleRowSelection: (rowId: string, index: number, shiftKey: boolean, data: any) => void;
  setSelectedRowIds: Dispatch<SetStateAction<string[]>>;
}

export const SelectionContext = React.createContext<SelectionContextType>({
  selectedRowIds: [],
  toggleRowSelection: (rowId: string, index: number, shiftKey: boolean, data: any) => {},
  setSelectedRowIds: (value: SetStateAction<string[]>) => {},
});

export const SelectionProvider: FC = ({ children }) => {
  const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
  const [firstRowIndex, setFirstRowIndex] = useState<number | null>(null);

  useEffect(() => {
    localStorage.setItem('selectedRowIds', JSON.stringify(selectedRowIds));
  }, [selectedRowIds]);

  useEffect(() => {
    const checkLocalStorage = () => {
      const storedRowIds = localStorage.getItem('selectedRowIds');
      const parsedRowIds = storedRowIds ? JSON.parse(storedRowIds) : [];

      if (JSON.stringify(parsedRowIds) !== JSON.stringify(selectedRowIds)) {
        setSelectedRowIds(parsedRowIds);
      }
    };

    const intervalId = setInterval(checkLocalStorage, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, [selectedRowIds]);

  const toggleRowSelection = (rowId: any, index: number, shiftKey: boolean, filteredAndSortedData: any) => {
    setSelectedRowIds((prevIds: any) => {
      if (shiftKey && firstRowIndex !== null) {
        const start = Math.min(firstRowIndex, index);
        const end = Math.max(firstRowIndex, index);
        const idsToSelect = filteredAndSortedData.slice(start, end + 1).map((row: any) => row.id);
        return Array.from(new Set([...prevIds, ...idsToSelect]));
      }

      if (prevIds.includes(rowId)) {
        return prevIds.filter((id: any) => id !== rowId);
      } else {
        setFirstRowIndex(index);
        return [...prevIds, rowId];
      }
    });
  };

  return (
    <SelectionContext.Provider value={{ selectedRowIds, toggleRowSelection, setSelectedRowIds }}>
      {children}
    </SelectionContext.Provider>
  );
};

Sign up to request clarification or add additional context in comments.

1 Comment

Any time anything in that provider changes it will re-render the children no?
0

Unnecessary re-render is causing the ui to jump around. You only want to update the row that was clicked, so only that row will be re-rendered in place while every row above and below it stays the same.

I have refactored your code to remove any unnecessary re-renders and clean things up logically.

import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import Table, { Column } from 'react-base-table';
import { Notification } from '@sms/plasma-ui';
import moment from 'moment';
import styled from 'styled-components';
import { pssApi } from '../../services';
import {
  ReorderColumnsDialog,
  SelectedRowsSummary,
  FilterDialog,
} from './components';
import {
  isSameSlab,
  extractAllSlabs,
  getData,
  handleData,
} from './utils';
import 'react-base-table/styles.css';
import html2canvas from 'html2canvas';

const TableComponent = ({
  columns,
  data,
  multiValueFilter,
  route,
}: {
  columns: any[];
  data: any[];
  multiValueFilter: any;
  route: any;
}) => {
  const [isDialogOpen, setDialogOpen] = useState(false);
  const [selectedColumn, setSelectedColumn] = useState(null);
  const [currentFilterValue, setCurrentFilterValue] = useState(undefined);
  const [selectedRowIds, setSelectedRowIds] = useState([]);
  const [firstRowIndex, setFirstRowIndex] = useState(null);
  const [reorderDialogOpen, setReorderDialogOpen] = useState(false);
  const [appliedFilters, setAppliedFilters] = useState({});
  const [sortConfig, setSortConfig] = useState(null);
  const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 });
  const [dialogsState, setDialogsState] = useState({});
  const [showSelectedOnTop, setShowSelectedOnTop] = useState(false);
  const [sequenceName, setSequenceName] = useState('');
  const [sequenceId, setSequenceId] = useState(null);
  const [startDate, setStartDate] = useState(undefined);
  const [casterType, setCasterType] = useState(null);
  const [oneCasterChecked, setOneCasterChecked] = useState(false);
  const [currentColumns, setCurrentColumns] = useState(() => getData(columns));
  const [isEdition, setIsEdition] = useState(false);
  const [dataScenario, setDataScenario] = useState(null);
  const scrollPositionRef = useRef(0);

  const sortStatesConstructorFunction = () => {
    return currentColumns.reduce((acc: any, column: any) => {
      if (column?.defaultSort) {
        acc[column.accessor] = column.defaultSort;
      }
      return acc;
    }, {});
  };

  const [sortStates, setSortStates] = useState(
    sortStatesConstructorFunction
  );

  const singleSort = (data: any[], dataKey: string, order: 'asc' | 'desc') => {
    return [...data].sort((a, b) => {
      const dir = order === 'asc' ? 1 : -1;
      if (a[dataKey] > b[dataKey]) return dir;
      if (a[dataKey] < b[dataKey]) return -dir;
      return 0;
    });
  };

  const handleToggleSelectedOnTop = useCallback(() => {
    setShowSelectedOnTop((prev) => !prev);
  }, []);

  const handleClearAllFilters = useCallback(() => {
    setAppliedFilters({});
    setSortConfig(null);
    setSortStates(sortStatesConstructorFunction);

    if (isEdition && sequenceId) {
      localStorage.removeItem(`sequenceTableState_${sequenceId}`);
    }
  }, [setAppliedFilters, setSortConfig, isEdition, sequenceId]);

  const filteredAndSortedData = useMemo(() => {
    let outputData = multiValueFilter(data, appliedFilters);

    if (sortConfig) {
      const { dataKey, order } = sortConfig;
      outputData = singleSort(outputData, dataKey, order);
    }

    if (showSelectedOnTop) {
      const selectedItems = [];
      const unselectedItems = [...outputData];

      for (const rowId of selectedRowIds) {
        const index = unselectedItems.findIndex((item) => item.id === rowId);
        if (index !== -1) {
          selectedItems.push(unselectedItems[index]);
          unselectedItems.splice(index, 1);
        }
      }

      return [...selectedItems, ...unselectedItems];
    }

    return outputData;
  }, [appliedFilters, data, multiValueFilter, sortConfig, selectedRowIds, showSelectedOnTop]);

  const setFilter = useCallback(
    (columnAccessor: string, value: string | string[] | { datetime: { startDate: moment.Moment | null; endDate: moment.Moment | null } } | undefined) => {
      setAppliedFilters((prev) => ({
        ...prev,
        [columnAccessor]: value,
      }));
    },
    [setAppliedFilters],
  );

  const onColumnSort = useCallback(
    ({ column, key, order }: any) => {
      let dataKey = column.dataKey ?? column.accessor;

      setSortStates((prev) => {
        if (prev[key] === 'desc') {
          const newState = { ...prev };
          delete newState[key];
          return newState;
        }
        return { ...prev, [key]: order };
      });

      setSortConfig({ key, order, dataKey });
    },
    [setSortStates, setSortConfig],
  );

  const updateDialogState = useCallback(
    (accessor: string, newValues: any) => {
      setDialogsState((prev) => ({
        ...prev,
        [accessor]: {
          ...prev[accessor],
          ...newValues,
        },
      }));
    },
    [setDialogsState],
  );

  const toggleRowSelection = useCallback(
    async (rowId: any, index: number, shiftKey: boolean) => {
      const scrollableElement = document.querySelector('[class*=BaseTable__body]');
      if (scrollableElement) {
        scrollPositionRef.current = scrollableElement.scrollTop;
      }

      setSelectedRowIds((prevIds: any) => {
        const newSelectedRowIds = [...prevIds];
        if (shiftKey && firstRowIndex !== null) {
          const start = Math.min(firstRowIndex, index);
          const end = Math.max(firstRowIndex, index);
          const idsToSelect = filteredAndSortedData.slice(start, end + 1).map((row: any) => row.id);
          newSelectedRowIds.push(...idsToSelect);
        } else if (newSelectedRowIds.includes(rowId)) {
          const index = newSelectedRowIds.indexOf(rowId);
          if (index > -1) {
            newSelectedRowIds.splice(index, 1);
          }
        } else {
          setFirstRowIndex(index);
          newSelectedRowIds.push(rowId);
        }

        return Array.from(new Set(newSelectedRowIds));
      });
    },
    [setSelectedRowIds, firstRowIndex, filteredAndSortedData],
  );

  const selectedRows = useMemo(() => {
    return selectedRowIds.map((rowId) => data.find((row) => row.id === rowId)).filter(Boolean);
  }, [data, selectedRowIds]);

  const currentDialogState = useMemo(() => dialogsState[selectedColumn?.accessor], [
    dialogsState,
    selectedColumn?.accessor,
  ]);

  useEffect(() => {
    const handleEditSequence = async (id: string) => {
      try {
        const { data: sequence } = await pssApi.getSequenceById({
          params: {
            id,
          },
        });

        if (!sequence) {
          return;
        }

        setSequenceName(sequence?.name);
        setStartDate(moment(sequence?.startDate));

        if (sequence?.scheduledCaster) {
          setCasterType(sequence?.scheduledCaster);
          setOneCasterChecked(true);
        } else {
          setOneCasterChecked(false);
        }

        const { data: dataScenario } = await pssApi.getScenarioMaterialList({ params: { sequenceScenarioId: id } });

        const allSlabs = extractAllSlabs(dataScenario);

        setDataScenario(dataScenario);

        const newSelectedRowIds: string[] = [];
        for (let i = 0; i < data.length; i++) {
          const row = data[i];
          if (isSameSlab(row, allSlabs)) {
            newSelectedRowIds.push(row.id);
          }
        }

        setSelectedRowIds(newSelectedRowIds);
        setSelectedRowIdsOld(newSelectedRowIds);
        setIsEdition(true);
        setSequenceId(id);
      } catch (error) {
        console.error('Error fetching sequence data:', error);
      }
    };

    const { id } = route.params;

    if (id) {
      handleEditSequence(id);
    } else {
      setSequenceName('');
      setSelectedRowIds([]);
      setSelectedRowIdsOld([]);
      setIsEdition(false);
      setSequenceId(null);
      setStartDate(undefined);
      setDataScenario(null);
      setOneCasterChecked(false);
      setCasterType(null);
    }
  }, [route]);

  const RowRenderer = useMemo(
    () => ({
      rowIndex,
      rowData,
      cells,
      toggleRowSelection,
      selectedRowIds,
    }: {
      cells: React.ReactNode[];
      columns: Column[];
      rowData: any;
      rowIndex: number;
      toggleRowSelection: (rowId: string, index: number, shiftKey: boolean) => void;
      selectedRowIds: string[];
    }) => {
      const isSelected = selectedRowIds.includes(rowData.id);

      const rowStyle = {
        backgroundColor: isSelected ? '#cce6ff' : '#fff',
        fontWeight: isSelected ? 'bold' : 'normal',
        display: 'flex',
        alignItems: 'stretch',
      };

      const wrappedCells = cells.map((cell: any, index: number) => {
        const cellProps = {
          ...cell.props,
          onClick: () => toggleRowSelection(rowData.id, rowIndex, event.shiftKey),
        };

        return React.cloneElement(cell, cellProps);
      });

      return (
        <div style={rowStyle}>
          {wrappedCells}
        </div>
      );
    },
    []
  );

  const CellRenderer = useMemo(
    () => ({ cellData }: { cellData: string }) => {
      const cellStyle = {
        display: 'flex',
        alignItems: 'center',
        padding: '8px',
        whiteSpace: 'nowrap',
        cursor: 'pointer',
        transition: 'background-color 0.2s',
      };

      return <div style={cellStyle}>{cellData}</div>;
    },
    []
  );

  const headerCellStyle = {
    fontWeight: 'bold',
    cursor: 'pointer',
    padding: '0 4px',
    alignItems: 'center',
    whiteSpace: 'nowrap',
  };

  const Container = styled.div`
    width: 100%;
    height: calc(100vh - 240px);
  `;

  const handleColumnResizeEnd = useCallback(
    ({ column, width }: { column: Column; width: number }) => {
      const newColumns = [...currentColumns];
      const columnIndex = newColumns.findIndex((col) => col.accessor === column.dataKey);

      if (columnIndex !== -1) {
        newColumns[columnIndex].defaultWidth = width;
        handleData(newColumns);
      }
    },
    [currentColumns]
  );

  const openFilterDialog = useCallback(
    (event: React.MouseEvent, column: Column) => {
      event.stopPropagation();
      setSelectedColumn(column);

      if (Object.keys(appliedFilters).length) {
        const currentFilter = appliedFilters[column.accessor];
        setCurrentFilterValue(currentFilter ? currentFilter : undefined);
      }

      const rect = event.currentTarget.getBoundingClientRect();
      const x = rect.right + 10;
      const y = rect.bottom + 10;

      const dialogWidth = 300;
      const dialogHeight = 200;
      const maxX = window.innerWidth - dialogWidth - 10;
      const maxY = window.innerHeight - dialogHeight - 10;
      const dialogX = Math.min(x, maxX);
      const dialogY = Math.min(y, maxY);

      setDialogPosition({ x: dialogX, y: dialogY });
      setDialogOpen(true);
    },
    [appliedFilters]
  );

  useEffect(() => {
    const restoreScroll = () => {
      const scrollableElement = document.querySelector('[class*=BaseTable__body]');

      if (scrollableElement) {
        scrollableElement.scrollTop = scrollPositionRef.current;
      }
    };

    const timeoutId = setTimeout(restoreScroll, 1);

    return () => {
      clearTimeout(timeoutId);
    };
  }, [selectedRowIds]);

  return (
    <Container>
      <Table
        width={width}
        height={height}
        data={filteredAndSortedData}
        rowKey="id"
        estimatedRowHeight={40}
        rowRenderer={RowRenderer}
      >
        {currentColumns
          .filter((column: Column) => !column?.hidden)
          .map((column: Column, index: number) => (
            <Column
              key={column.accessor}
              dataKey={column.accessor}
              width={column.defaultWidth}
              headerRenderer={() => (
                <div
                  style={headerCellStyle}
                  onClick={(event) => openFilterDialog(event, column)}
                >
                  {column.Header}
                </div>
              )}
              cellRenderer={(props: {
                cellData: any;
                columns: Column[];
                column: Column;
                columnIndex: number;
                rowData: unknown;
                rowIndex: number;
                container: Table;
              }) => (
                <CellRenderer cellData={props.cellData} />
              )}
            />
          ))}
      </Table>
      {isDialogOpen && (
        <FilterDialog
          onClose={() => setDialogOpen(false)}
          setFilter={setFilter}
          position={dialogPosition}
          setCurrentFilterValue={setCurrentFilterValue}
          data={data}
          dialogState={currentDialogState || {}}
          updateDialogState={updateDialogState}
          onColumnSort={onColumnSort}
        />
      )}
      {reorderDialogOpen && (
        <ReorderColumnsDialog
          onClose={() => setReorderDialogOpen(false)}
          onUpdateColumns={setCurrentColumns}
        />
      )}
    </Container>
  );
};

export default React.memo(TableComponent);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.