import React, { useState } from 'react'; import isEqual from 'react-fast-compare'; import { ExpandedState, getExpandedRowModel, RowModel, Table, useReactTable, } from '@tanstack/react-table'; import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, } from '@tanstack/react-table'; import omit from 'lodash/omit'; import { usePrevious } from '@leafygreen-ui/hooks'; import { spacing } from '@leafygreen-ui/tokens'; import { TableHeaderCheckbox } from './TableHeaderCheckbox'; import { TableRowCheckbox } from './TableRowCheckbox'; import { LeafyGreenTableOptions, LGRowData } from './useLeafyGreenTable.types'; import { LeafyGreenTable, LGColumnDef, LGTableDataType } from '.'; const CHECKBOX_WIDTH = spacing[1000]; function useLeafyGreenTable({ data, columns: columnsProp, hasSelectableRows, withPagination = false, allowSelectAll = true, ...rest }: LeafyGreenTableOptions): LeafyGreenTable { const [expanded, setExpanded] = useState( (rest.initialState?.expanded as ExpandedState) ?? {}, ); const prevColumns = usePrevious(columnsProp); /** * Performance optimization flag that controls table row memoization inside `MemoizedInternalRowWithRT`. * * - `true`: Rows are memoized and only re-render when their data/props change, excluding children. * - `false`: All rows re-render (when column definitions change). This bypasses memoization and ensures that rows pick up new configurations immediately. * * On initial load, prevColumns is undefined, so we enable memoization (return true) * to avoid unnecessary re-renders after the first load */ const shouldMemoizeRows = prevColumns === undefined || isEqual(prevColumns, columnsProp); /** * A `ColumnDef` object injected into `useReactTable`'s `columns` option when the user is using selectable rows. */ const baseSelectColumnConfig: LGColumnDef = { id: 'select', size: CHECKBOX_WIDTH, header: TableHeaderCheckbox, cell: TableRowCheckbox, }; const hasSortableColumns = React.useMemo( () => columnsProp.some(propCol => !!propCol.enableSorting), [columnsProp], ); const selectColumnConfig = allowSelectAll ? baseSelectColumnConfig : omit(baseSelectColumnConfig, 'header'); const columns = React.useMemo>>( () => [ ...(hasSelectableRows ? [selectColumnConfig as LGColumnDef] : []), ...columnsProp, ], [columnsProp, hasSelectableRows, selectColumnConfig], ); /** * Custom getExpandedRowModel that manipulates rows to include expandedContent. */ function getLGExpandedRowModel>() { return (table: Table) => { const baseExpandedRowModel = getExpandedRowModel()(table); // Return a function that computes the custom RowModel return () => { // Get the default expanded row model by invoking baseExpandedRowModel const rowModel = baseExpandedRowModel(); const modifiedRows = [...rowModel.rows]; for (let i = 0; i < modifiedRows.length; i++) { if ( modifiedRows[i].original.renderExpandedContent && modifiedRows[i].getIsExpanded() ) { const expandedData = { ...modifiedRows[i], id: `${modifiedRows[i].id}-expandedContent`, isExpandedContent: true, }; modifiedRows.splice(i + 1, 0, expandedData); i++; // Increment index to skip the newly added item } } return { ...rowModel, rows: modifiedRows, } as RowModel; }; }; } const table = useReactTable>({ data, columns, getCoreRowModel: getCoreRowModel(), getRowCanExpand: row => { return !!row.original.renderExpandedContent || !!row.subRows?.length; }, enableExpanding: true, enableSortingRemoval: hasSortableColumns ? true : undefined, getSubRows: row => row.subRows, getSortedRowModel: getSortedRowModel(), getPaginationRowModel: withPagination ? getPaginationRowModel() : undefined, onExpandedChange: setExpanded, getExpandedRowModel: getLGExpandedRowModel(), ...rest, state: { expanded, ...rest.state, }, }); return { ...table, hasSelectableRows, shouldMemoizeRows, } as LeafyGreenTable; } export default useLeafyGreenTable;