/** * ViewHolder is a core component in FlashList that manages individual item rendering and layout. * It handles the rendering of list items, separators, and manages layout updates for each item. * The component is memoized to prevent unnecessary re-renders and includes layout comparison logic. */ import { LayoutChangeEvent } from "react-native"; import React, { RefObject, useCallback, useLayoutEffect, useMemo, useRef, } from "react"; import { FlashListProps, RenderTarget } from "../FlashListProps"; import { RVDimension, RVLayout } from "./layout-managers/LayoutManager"; import { CompatView } from "./components/CompatView"; import { getInvertedTransformStyle } from "./utils/getInvertedTransformStyle"; /** * Props interface for the ViewHolder component * @template TItem - The type of item being rendered in the list */ export interface ViewHolderProps { /** Index of the item in the data array */ index: number; /** Layout information for positioning and sizing the item */ layout: RVLayout; /** Map to store refs for each ViewHolder instance, keyed by index */ refHolder: Map>; /** Additional data passed to renderItem that can trigger re-renders */ extraData: any; /** Specifies the rendering target (e.g., "Cell", "StickyHeader") */ target: RenderTarget; /** The actual item data to be rendered */ item: TItem; /** The next item in the list, used for rendering separators */ trailingItem: TItem | undefined; /** Function to render the item content */ renderItem: FlashListProps["renderItem"]; /** Optional custom component to wrap each item */ CellRendererComponent?: FlashListProps["CellRendererComponent"]; /** Optional component to render between items */ ItemSeparatorComponent?: FlashListProps["ItemSeparatorComponent"]; /** Whether the list is horizontal or vertical */ horizontal?: FlashListProps["horizontal"]; /** Callback when the item's size changes */ onSizeChanged?: (index: number, size: RVDimension) => void; /** Whether this item should be hidden (likely because it is associated with the active sticky header) */ hidden: boolean; /** Whether the list is inverted */ inverted?: FlashListProps["inverted"]; } /** * Internal ViewHolder component that handles the actual rendering of list items * @template TItem - The type of item being rendered in the list */ const ViewHolderInternal = (props: ViewHolderProps) => { // create ref for View const viewRef = useRef(null); const { index, refHolder, layout, onSizeChanged, renderItem, extraData, item, target, CellRendererComponent, ItemSeparatorComponent, trailingItem, horizontal, hidden, inverted, } = props; useLayoutEffect(() => { refHolder.set(index, viewRef); return () => { if (refHolder.get(index) === viewRef) { refHolder.delete(index); } }; }, [index, refHolder]); const onLayout = useCallback( (event: LayoutChangeEvent) => { onSizeChanged?.(index, event.nativeEvent.layout); }, [index, onSizeChanged] ); const separator = useMemo(() => { return ItemSeparatorComponent && trailingItem !== undefined ? ( ) : null; }, [ItemSeparatorComponent, item, trailingItem]); // console.log("ViewHolder re-render", index); const children = useMemo(() => { return renderItem?.({ item, index, extraData, target }) ?? null; // TODO: Test more thoroughly // We don't really to re-render the children when the index changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [item, extraData, target, renderItem]); const invertedTransformStyle = inverted ? getInvertedTransformStyle(horizontal) : undefined; const style = { flexDirection: horizontal ? "row" : "column", position: target === "StickyHeader" ? "relative" : "absolute", width: layout.enforcedWidth ? layout.width : undefined, height: layout.enforcedHeight ? layout.height : undefined, minHeight: layout.minHeight, minWidth: layout.minWidth, maxHeight: layout.maxHeight, maxWidth: layout.maxWidth, left: layout.x, top: layout.y, opacity: hidden ? 0 : 1, ...invertedTransformStyle, } as const; // TODO: Fix this type issue const CompatContainer = (CellRendererComponent ?? CompatView) as unknown as any; return ( {children} {separator} ); }; /** * Memoized ViewHolder component that prevents unnecessary re-renders by comparing props * @template TItem - The type of item being rendered in the list */ export const ViewHolder = React.memo( ViewHolderInternal, (prevProps, nextProps) => { // compare all props and spread layout return ( prevProps.index === nextProps.index && areLayoutsEqual(prevProps.layout, nextProps.layout) && prevProps.refHolder === nextProps.refHolder && prevProps.onSizeChanged === nextProps.onSizeChanged && prevProps.extraData === nextProps.extraData && prevProps.target === nextProps.target && prevProps.item === nextProps.item && prevProps.renderItem === nextProps.renderItem && prevProps.CellRendererComponent === nextProps.CellRendererComponent && prevProps.ItemSeparatorComponent === nextProps.ItemSeparatorComponent && prevProps.trailingItem === nextProps.trailingItem && prevProps.horizontal === nextProps.horizontal && prevProps.hidden === nextProps.hidden && prevProps.inverted === nextProps.inverted ); } ); /** * Compares two RVLayout objects to determine if they are equal * Used in the memo comparison function to prevent unnecessary re-renders * @param prevLayout - Previous layout object * @param nextLayout - Next layout object * @returns boolean indicating if layouts are equal */ function areLayoutsEqual(prevLayout: RVLayout, nextLayout: RVLayout): boolean { return ( prevLayout.x === nextLayout.x && prevLayout.y === nextLayout.y && prevLayout.width === nextLayout.width && prevLayout.height === nextLayout.height && prevLayout.enforcedWidth === nextLayout.enforcedWidth && prevLayout.enforcedHeight === nextLayout.enforcedHeight && prevLayout.minWidth === nextLayout.minWidth && prevLayout.minHeight === nextLayout.minHeight && prevLayout.maxWidth === nextLayout.maxWidth && prevLayout.maxHeight === nextLayout.maxHeight ); }