refactor list internal state to target rerenders on change

This commit is contained in:
jeffvli
2025-11-26 15:46:17 -08:00
parent 10c9bec2cf
commit a238927749
8 changed files with 421 additions and 168 deletions
+20 -11
View File
@@ -9,7 +9,11 @@ import styles from './item-card.module.css';
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
import {
ItemListStateActions,
useItemDraggingState,
useItemSelectionState,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes';
@@ -141,10 +145,11 @@ const CompactItemCard = ({
withControls,
}: ItemCardDerivativeProps) => {
const [showControls, setShowControls] = useState(false);
const isSelected =
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
? internalState.isSelected(internalState.extractRowId(data) || '')
: false;
? internalState.extractRowId(data)
: undefined;
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
const handleClick = useDoubleClick({
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
@@ -345,10 +350,11 @@ const DefaultItemCard = ({
withControls,
}: ItemCardDerivativeProps) => {
const [showControls, setShowControls] = useState(false);
const isSelected =
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
? internalState.isSelected(internalState.extractRowId(data) || '')
: false;
? internalState.extractRowId(data)
: undefined;
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
const handleClick = useDoubleClick({
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
@@ -549,10 +555,11 @@ const PosterItemCard = ({
withControls,
}: ItemCardDerivativeProps) => {
const [showControls, setShowControls] = useState(false);
const isSelected =
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
? internalState.isSelected(internalState.extractRowId(data) || '')
: false;
? internalState.extractRowId(data)
: undefined;
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
drag: {
@@ -597,7 +604,9 @@ const PosterItemCard = ({
isEnabled: !!enableDrag && !!data,
});
const isDragging = data && internalState ? internalState.isDragging(data.id) : isDraggingLocal;
const itemId = data && internalState ? data.id : undefined;
const isDraggingState = useItemDraggingState(internalState, itemId);
const isDragging = isDraggingState || isDraggingLocal;
const handleClick = useDoubleClick({
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
@@ -5,6 +5,7 @@ import styles from './expanded-list-item.module.css';
import {
ItemListStateActions,
ItemListStateItem,
useItemListStateSubscription,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { ExpandedAlbumListItem } from '/@/renderer/features/albums/components/expanded-album-list-item';
import { Spinner } from '/@/shared/components/spinner/spinner';
@@ -16,7 +17,9 @@ interface ExpandedListItemProps {
}
export const ExpandedListItem = ({ internalState, itemType }: ExpandedListItemProps) => {
const expandedItems = internalState.getExpanded();
const expandedItems = useItemListStateSubscription(internalState, () =>
internalState ? internalState.getExpandedItemsCached() : [],
);
const currentItem = expandedItems[0];
if (!currentItem) {
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useReducer } from 'react';
import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react';
import { itemListSelectors } from '/@/renderer/components/item-list/helpers/item-list-reducer-utils';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -84,6 +84,7 @@ export interface ItemListStateActions {
getDraggingIds: () => string[];
getExpanded: () => unknown[];
getExpandedIds: () => string[];
getExpandedItemsCached: () => unknown[];
getSelected: () => unknown[];
getSelectedIds: () => string[];
getVersion: () => number;
@@ -281,11 +282,122 @@ export const initialItemListState: ItemListState = {
version: 0,
};
/**
* External store for item list state that doesn't cause React rerenders
* Components can subscribe to specific state slices using useSyncExternalStore
*/
class ItemListStateStore {
// Cache for derived values to prevent unnecessary rerenders
private expandedItemsCache: null | unknown[] = null;
private expandedItemsCacheVersion: number = -1;
private listeners = new Set<() => void>();
private state: ItemListState = { ...initialItemListState };
dispatch(action: ItemListAction): void {
this.state = itemListReducer(this.state, action);
// Invalidate caches when state changes
this.expandedItemsCache = null;
// Notify all subscribers
this.listeners.forEach((listener) => listener());
}
getExpandedItems(): unknown[] {
// Return cached array if state version hasn't changed
if (
this.expandedItemsCache !== null &&
this.expandedItemsCacheVersion === this.state.version
) {
return this.expandedItemsCache;
}
// Create new array and cache it
this.expandedItemsCache = Array.from(this.state.expandedItems.values());
this.expandedItemsCacheVersion = this.state.version;
return this.expandedItemsCache;
}
getState(): ItemListState {
return this.state;
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
}
/**
* Hook to subscribe to specific state changes in the item list state
* Use this in components that need to rerender when state changes
*/
export const useItemListStateSubscription = <T>(
internalState: ItemListStateActions | undefined,
selector: (state: ItemListState | null) => T,
): T => {
const store = internalState ? ((internalState as any).__store as ItemListStateStore) : null;
return useSyncExternalStore(
store?.subscribe.bind(store) || (() => () => {}), // Return no-op unsubscribe if no store
() => selector(store?.getState() || null),
);
};
/**
* Hook to subscribe to selection state for a specific item
* Use this in components that need to rerender when a specific item's selection changes
*/
export const useItemSelectionState = (
internalState: ItemListStateActions | undefined,
rowId: string | undefined,
): boolean => {
return useItemListStateSubscription(internalState, (state) =>
state && rowId ? state.selected.has(rowId) : false,
);
};
/**
* Hook to subscribe to expansion state for a specific item
* Use this in components that need to rerender when a specific item's expansion changes
*/
export const useItemExpansionState = (
internalState: ItemListStateActions | undefined,
rowId: string | undefined,
): boolean => {
return useItemListStateSubscription(internalState, (state) =>
state && rowId ? state.expanded.has(rowId) : false,
);
};
/**
* Hook to subscribe to dragging state for a specific item
* Use this in components that need to rerender when a specific item's dragging state changes
*/
export const useItemDraggingState = (
internalState: ItemListStateActions | undefined,
rowId: string | undefined,
): boolean => {
return useItemListStateSubscription(internalState, (state) =>
state && rowId ? state.dragging.has(rowId) : false,
);
};
export const useItemListState = (
getDataFn?: () => unknown[],
extractRowId?: (item: unknown) => string | undefined,
): ItemListStateActions => {
const [state, dispatch] = useReducer(itemListReducer, initialItemListState);
// Create store instance (stable across rerenders)
const storeRef = useRef<ItemListStateStore | null>(null);
if (!storeRef.current) {
storeRef.current = new ItemListStateStore();
}
const store = storeRef.current;
// DON'T subscribe here - this prevents rerenders when state changes
// Components that need to react should use useItemListStateSubscription
// Get current state (this doesn't cause rerenders, it's just reading from the store)
const getCurrentState = useCallback(() => store.getState(), [store]);
const extractRowIdFn = useCallback(
(item: unknown) => {
@@ -303,138 +415,156 @@ export const useItemListState = (
const setExpanded = useCallback(
(items: ItemListStateItemWithRequiredProperties[]) => {
dispatch({
store.dispatch({
extractRowId: extractRowIdFn,
payload: items,
type: 'SET_EXPANDED',
});
},
[extractRowIdFn],
[store, extractRowIdFn],
);
const setDragging = useCallback(
(items: ItemListStateItemWithRequiredProperties[]) => {
dispatch({
store.dispatch({
extractRowId: extractRowIdFn,
payload: items,
type: 'SET_DRAGGING',
});
},
[extractRowIdFn],
[store, extractRowIdFn],
);
const setSelected = useCallback(
(items: ItemListStateItemWithRequiredProperties[]) => {
dispatch({
store.dispatch({
extractRowId: extractRowIdFn,
payload: items,
type: 'SET_SELECTED',
});
},
[extractRowIdFn],
[store, extractRowIdFn],
);
const toggleExpanded = useCallback(
(item: ItemListStateItemWithRequiredProperties) => {
dispatch({
store.dispatch({
extractRowId: extractRowIdFn,
payload: item,
type: 'TOGGLE_EXPANDED',
});
},
[extractRowIdFn],
[store, extractRowIdFn],
);
const toggleSelected = useCallback(
(item: ItemListStateItemWithRequiredProperties) => {
dispatch({
store.dispatch({
extractRowId: extractRowIdFn,
payload: item,
type: 'TOGGLE_SELECTED',
});
},
[extractRowIdFn],
[store, extractRowIdFn],
);
// These methods read from the store without subscribing, so they don't cause rerenders
const isExpanded = useCallback(
(rowId: string) => {
const state = getCurrentState();
return itemListSelectors.isExpanded(state, rowId);
},
[state],
[getCurrentState],
);
const isSelected = useCallback(
(rowId: string) => {
const state = getCurrentState();
return itemListSelectors.isSelected(state, rowId);
},
[state],
[getCurrentState],
);
const getExpanded = useCallback(() => {
const state = getCurrentState();
return itemListSelectors.getExpanded(state);
}, [state]);
}, [getCurrentState]);
const getExpandedItemsCached = useCallback(() => {
return store.getExpandedItems();
}, [store]);
const getDragging = useCallback(() => {
const state = getCurrentState();
return itemListSelectors.getDragging(state);
}, [state]);
}, [getCurrentState]);
const getSelected = useCallback(() => {
const state = getCurrentState();
const selectedItems = itemListSelectors.getSelected(state);
const data = getDataFn ? getDataFn() : [];
return sortByDataOrder(selectedItems, data, extractRowIdFn, false);
}, [state, getDataFn, extractRowIdFn]);
}, [getCurrentState, getDataFn, extractRowIdFn]);
const getDraggingIds = useCallback(() => {
const state = getCurrentState();
return Array.from(state.dragging);
}, [state.dragging]);
}, [getCurrentState]);
const getExpandedIds = useCallback(() => {
const state = getCurrentState();
return Array.from(state.expanded);
}, [state.expanded]);
}, [getCurrentState]);
const getSelectedIds = useCallback(() => {
const state = getCurrentState();
const selectedIds = Array.from(state.selected);
const data = getDataFn ? getDataFn() : [];
return sortByDataOrder(selectedIds, data, extractRowIdFn, true);
}, [state.selected, getDataFn, extractRowIdFn]);
}, [getCurrentState, getDataFn, extractRowIdFn]);
const clearExpanded = useCallback(() => {
dispatch({ type: 'CLEAR_EXPANDED' });
}, []);
store.dispatch({ type: 'CLEAR_EXPANDED' });
}, [store]);
const clearDragging = useCallback(() => {
dispatch({ type: 'CLEAR_DRAGGING' });
}, []);
store.dispatch({ type: 'CLEAR_DRAGGING' });
}, [store]);
const clearSelected = useCallback(() => {
dispatch({ type: 'CLEAR_SELECTED' });
}, []);
store.dispatch({ type: 'CLEAR_SELECTED' });
}, [store]);
const clearAll = useCallback(() => {
dispatch({ type: 'CLEAR_ALL' });
}, []);
store.dispatch({ type: 'CLEAR_ALL' });
}, [store]);
const getVersion = useCallback(() => {
const state = getCurrentState();
return itemListSelectors.getVersion(state);
}, [state]);
}, [getCurrentState]);
const hasExpanded = useCallback(() => {
const state = getCurrentState();
return itemListSelectors.hasAnyExpanded(state);
}, [state]);
}, [getCurrentState]);
const hasDragging = useCallback(() => {
const state = getCurrentState();
return itemListSelectors.hasAnyDragging(state);
}, [state]);
}, [getCurrentState]);
const hasSelected = useCallback(() => {
const state = getCurrentState();
return itemListSelectors.hasAnySelected(state);
}, [state]);
}, [getCurrentState]);
const isDragging = useCallback(
(rowId: string) => {
const state = getCurrentState();
return itemListSelectors.isDragging(state, rowId);
},
[state],
[getCurrentState],
);
const getData = useCallback(() => {
@@ -457,8 +587,12 @@ export const useItemListState = (
[getDataFn, extractRowId],
);
return useMemo(
() => ({
// Expose the store so components can subscribe if needed
// Store it in the actions object for access
const actions = useMemo(() => {
const actionsObj = {
__getState: getCurrentState,
__store: store,
clearAll,
clearDragging,
clearExpanded,
@@ -470,6 +604,7 @@ export const useItemListState = (
getDraggingIds,
getExpanded,
getExpandedIds,
getExpandedItemsCached,
getSelected,
getSelectedIds,
getVersion,
@@ -484,33 +619,41 @@ export const useItemListState = (
setSelected,
toggleExpanded,
toggleSelected,
}),
[
clearAll,
clearDragging,
clearExpanded,
clearSelected,
extractRowIdFn,
findItemIndex,
getData,
getDragging,
getDraggingIds,
getExpanded,
getExpandedIds,
getSelected,
getSelectedIds,
getVersion,
hasDragging,
hasExpanded,
hasSelected,
isDragging,
isExpanded,
isSelected,
setDragging,
setExpanded,
setSelected,
toggleExpanded,
toggleSelected,
],
);
} as ItemListStateActions & {
__getState: () => ItemListState;
__store: ItemListStateStore;
};
return actionsObj;
}, [
clearAll,
clearDragging,
clearExpanded,
clearSelected,
extractRowIdFn,
findItemIndex,
getData,
getDragging,
getDraggingIds,
getExpanded,
getExpandedIds,
getExpandedItemsCached,
getSelected,
getSelectedIds,
getVersion,
hasDragging,
hasExpanded,
hasSelected,
isDragging,
isExpanded,
isSelected,
setDragging,
setExpanded,
setSelected,
toggleExpanded,
toggleSelected,
store,
getCurrentState,
]);
return actions;
};
@@ -18,7 +18,10 @@ import styles from './item-detail-list.module.css';
import { ItemDetail } from '/@/renderer/components/item-detail/item-detail';
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
import { useItemListState } from '/@/renderer/components/item-list/helpers/item-list-state';
import {
useItemListState,
useItemListStateSubscription,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { useElementSize } from '/@/shared/hooks/use-element-size';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -118,15 +121,17 @@ export const ItemDetailList = ({
}
}, [itemDetailRef, initialize]);
const hasExpanded = internalState.hasExpanded();
const hasExpanded = useItemListStateSubscription(internalState, (state) =>
state ? state.expanded.size > 0 : false,
);
const handleExpand = useCallback(
(_e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => {
if (item && typeof item === 'object' && 'id' in item && 'serverId' in item) {
internalState.toggleExpanded({
_itemType: itemType,
_serverId: item.serverId as string,
id: item.id as string,
itemType: itemType,
});
}
},
@@ -39,6 +39,7 @@ import {
ItemListStateActions,
ItemListStateItemWithRequiredProperties,
useItemListState,
useItemListStateSubscription,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
import { animationProps } from '/@/shared/components/animations/animation-props';
@@ -320,8 +321,6 @@ const BaseItemGridList = ({
},
});
const hasExpanded = internalState.hasExpanded();
const tableMetaRef = useRef<null | {
columnCount: number;
itemHeight: number;
@@ -691,13 +690,7 @@ const BaseItemGridList = ({
/>
)}
</AutoSizer>
<AnimatePresence>
{hasExpanded && (
<ExpandedListContainer>
<ExpandedListItem internalState={internalState} itemType={itemType} />
</ExpandedListContainer>
)}
</AnimatePresence>
<ExpandedContainer internalState={internalState} itemType={itemType} />
</motion.div>
);
};
@@ -754,3 +747,25 @@ const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
export const ItemGridList = memo(BaseItemGridList);
ItemGridList.displayName = 'ItemGridList';
const ExpandedContainer = ({
internalState,
itemType,
}: {
internalState: ItemListStateActions;
itemType: LibraryItem;
}) => {
const hasExpanded = useItemListStateSubscription(internalState, (state) =>
state ? state.expanded.size > 0 : false,
);
return (
<AnimatePresence initial={false}>
{hasExpanded && (
<ExpandedListContainer>
<ExpandedListItem internalState={internalState} itemType={itemType} />
</ExpandedListContainer>
)}
</AnimatePresence>
);
};
@@ -17,6 +17,10 @@ import styles from './item-table-list-column.module.css';
import i18n from '/@/i18n/i18n';
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
import {
useItemDraggingState,
useItemSelectionState,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { ActionsColumn } from '/@/renderer/components/item-list/item-table-list/columns/actions-column';
import { AlbumArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-artists-column';
import { AlbumColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-column';
@@ -294,10 +298,16 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
isEnabled: shouldEnableDrag,
});
const isDragging =
const itemRowId =
item && typeof item === 'object' && 'id' in item && props.internalState
? props.internalState.isDragging((item as any).id)
: isDraggingLocal;
? props.internalState.extractRowId(item)
: undefined;
const isDraggingState = useItemDraggingState(
props.internalState,
itemRowId ||
(item && typeof item === 'object' && 'id' in item ? (item as any).id : undefined),
);
const isDragging = props.internalState ? isDraggingState : isDraggingLocal;
const controls = props.controls;
@@ -452,10 +462,11 @@ export const TableColumnTextContainer = (
const isDataRow = props.enableHeader ? props.rowIndex > 0 : true;
const dataIndex = props.enableHeader ? props.rowIndex - 1 : props.rowIndex;
const item = isDataRow ? props.data[props.rowIndex] : null;
const isSelected =
const itemRowId =
item && typeof item === 'object' && 'id' in item
? props.internalState.isSelected(props.internalState.extractRowId(item) || '')
: false;
? props.internalState.extractRowId(item)
: undefined;
const isSelected = useItemSelectionState(props.internalState, itemRowId || undefined);
const isDragging = props.isDragging ?? false;
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
@@ -664,10 +675,11 @@ export const TableColumnContainer = (
const isDataRow = props.enableHeader ? props.rowIndex > 0 : true;
const dataIndex = props.enableHeader ? props.rowIndex - 1 : props.rowIndex;
const item = isDataRow ? props.data[props.rowIndex] : null;
const isSelected =
const itemRowId =
item && typeof item === 'object' && 'id' in item
? props.internalState.isSelected(props.internalState.extractRowId(item) || '')
: false;
? props.internalState.extractRowId(item)
: undefined;
const isSelected = useItemSelectionState(props.internalState, itemRowId || undefined);
const isDragging = props.isDragging ?? false;
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
@@ -29,6 +29,7 @@ import {
ItemListStateActions,
ItemListStateItemWithRequiredProperties,
useItemListState,
useItemListStateSubscription,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
@@ -1583,8 +1584,6 @@ const BaseItemTableList = ({
const internalState = useItemListState(getDataFn, extractRowId);
const hasExpanded = internalState.hasExpanded();
// Helper function to get ItemListStateItemWithRequiredProperties (rowId is separate, not part of item)
const getStateItem = useCallback(
(item: any): ItemListStateItemWithRequiredProperties | null => {
@@ -2169,17 +2168,33 @@ const BaseItemTableList = ({
totalColumnCount={totalColumnCount}
totalRowCount={totalRowCount}
/>
<AnimatePresence initial={false}>
{hasExpanded && (
<ExpandedListContainer>
<ExpandedListItem internalState={internalState} itemType={itemType} />
</ExpandedListContainer>
)}
</AnimatePresence>
<ExpandedContainer internalState={internalState} itemType={itemType} />
</motion.div>
);
};
export const ItemTableList = memo(BaseItemTableList);
const ExpandedContainer = ({
internalState,
itemType,
}: {
internalState: ItemListStateActions;
itemType: LibraryItem;
}) => {
const hasExpanded = useItemListStateSubscription(internalState, (state) =>
state ? state.expanded.size > 0 : false,
);
return (
<AnimatePresence initial={false}>
{hasExpanded && (
<ExpandedListContainer>
<ExpandedListItem internalState={internalState} itemType={itemType} />
</ExpandedListContainer>
)}
</AnimatePresence>
);
};
ItemTableList.displayName = 'ItemTableList';
@@ -1,11 +1,15 @@
import clsx from 'clsx';
import { useId, useMemo } from 'react';
import { memo, useId, useMemo } from 'react';
import styles from './simple-item-table.module.css';
import { createExtractRowId } from '/@/renderer/components/item-list/helpers/extract-row-id';
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
import { useItemListState } from '/@/renderer/components/item-list/helpers/item-list-state';
import {
ItemListStateActions,
useItemListState,
useItemSelectionState,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
@@ -176,68 +180,115 @@ export const SimpleItemTable = ({
</Table.Thead>
)}
<Table.Tbody>
{data.map((item, rowIndex) => {
const adjustedRowIndex = enableHeader ? rowIndex + 1 : rowIndex;
const isSelected =
item && typeof item === 'object' && 'id' in item
? internalState.isSelected(internalState.extractRowId(item) || '')
: false;
const isLastRow = rowIndex === data.length - 1;
return (
<Table.Tr
className={clsx({
[styles.alternateRowEven]:
enableAlternateRowColors && rowIndex % 2 === 0,
[styles.alternateRowOdd]:
enableAlternateRowColors && rowIndex % 2 === 1,
[styles.rowHover]: enableRowHoverHighlight,
[styles.rowSelected]: isSelected,
[styles.withHorizontalBorder]:
enableHorizontalBorders && enableHeader && !isLastRow,
})}
data-row-index={`${tableId}-${adjustedRowIndex}`}
key={internalState.extractRowId(item) || rowIndex}
>
{parsedColumns.map((column, columnIndex) => {
const isLastColumn = columnIndex === parsedColumns.length - 1;
return (
<Table.Td
className={clsx({
[styles.withVerticalBorder]:
enableVerticalBorders && !isLastColumn,
})}
key={column.id}
style={{
textAlign:
column.align === 'start'
? 'left'
: column.align === 'end'
? 'right'
: 'center',
width: column.width,
}}
>
<ItemTableListColumn
{...tableItemProps}
ariaAttributes={{
'aria-colindex': columnIndex + 1,
role: 'gridcell',
}}
columnIndex={columnIndex}
rowIndex={adjustedRowIndex}
style={{ width: column.width }}
/>
</Table.Td>
);
})}
</Table.Tr>
);
})}
{data.map((item, rowIndex) => (
<SimpleItemTableRow
adjustedRowIndex={enableHeader ? rowIndex + 1 : rowIndex}
enableAlternateRowColors={enableAlternateRowColors}
enableHeader={enableHeader}
enableHorizontalBorders={enableHorizontalBorders}
enableRowHoverHighlight={enableRowHoverHighlight}
enableVerticalBorders={enableVerticalBorders}
internalState={internalState}
isLastRow={rowIndex === data.length - 1}
item={item}
key={internalState.extractRowId(item) || rowIndex}
parsedColumns={parsedColumns}
rowIndex={rowIndex}
tableId={tableId}
tableItemProps={tableItemProps}
/>
))}
</Table.Tbody>
</Table>
</div>
);
};
interface SimpleItemTableRowProps {
adjustedRowIndex: number;
enableAlternateRowColors: boolean;
enableHeader: boolean;
enableHorizontalBorders: boolean;
enableRowHoverHighlight: boolean;
enableVerticalBorders: boolean;
internalState: ItemListStateActions;
isLastRow: boolean;
item: unknown;
parsedColumns: ReturnType<typeof parseTableColumns>;
rowIndex: number;
tableId: string;
tableItemProps: TableItemProps;
}
const SimpleItemTableRow = memo(
({
adjustedRowIndex,
enableAlternateRowColors,
enableHeader,
enableHorizontalBorders,
enableRowHoverHighlight,
enableVerticalBorders,
internalState,
isLastRow,
item,
parsedColumns,
rowIndex,
tableId,
tableItemProps,
}: SimpleItemTableRowProps) => {
const itemRowId =
item && typeof item === 'object' && 'id' in item
? internalState.extractRowId(item)
: undefined;
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
return (
<Table.Tr
className={clsx({
[styles.alternateRowEven]: enableAlternateRowColors && rowIndex % 2 === 0,
[styles.alternateRowOdd]: enableAlternateRowColors && rowIndex % 2 === 1,
[styles.rowHover]: enableRowHoverHighlight,
[styles.rowSelected]: isSelected,
[styles.withHorizontalBorder]:
enableHorizontalBorders && enableHeader && !isLastRow,
})}
data-row-index={`${tableId}-${adjustedRowIndex}`}
>
{parsedColumns.map((column, columnIndex) => {
const isLastColumn = columnIndex === parsedColumns.length - 1;
return (
<Table.Td
className={clsx({
[styles.withVerticalBorder]: enableVerticalBorders && !isLastColumn,
})}
key={column.id}
style={{
textAlign:
column.align === 'start'
? 'left'
: column.align === 'end'
? 'right'
: 'center',
width: column.width,
}}
>
<ItemTableListColumn
{...tableItemProps}
ariaAttributes={{
'aria-colindex': columnIndex + 1,
role: 'gridcell',
}}
columnIndex={columnIndex}
rowIndex={adjustedRowIndex}
style={{ width: column.width }}
/>
</Table.Td>
);
})}
</Table.Tr>
);
},
);
SimpleItemTableRow.displayName = 'SimpleItemTableRow';