export function useVirtualList()

in uui-core/src/hooks/useVirtualList/useVirtualList.ts [12:229]


export function useVirtualList<List extends HTMLElement = any, ScrollContainer extends HTMLElement = any>(
    props: UseVirtualListProps,
): UseVirtualListApi<List, ScrollContainer> {
    const {
        onValueChange,
        value,
        rowsCount,
        onScroll,
        blockSize = 20,
        overdrawRows = 20,
        rowsSelector,
    } = props;
    const [estimatedHeight, setEstimatedHeight] = React.useState<number>(0);
    const [listOffset, setListOffset] = React.useState<number>();
    const [scrolledTo, setScrolledTo] = React.useState<ScrollToConfig>(null);
    const listContainer = React.useRef<List>();
    const scrollContainer = React.useRef<ScrollContainer>();
    const rowHeights = React.useRef<number[]>([]);
    const rowOffsets = React.useRef<number[]>([]);
    const scrollContainerHeightChangesCount = React.useRef<number>(0);
    const scrollContainerHeightIsNotLimited = React.useRef(false);
    const prevScrollContainerClientHeight = usePrevious(scrollContainer.current?.clientHeight);

    const virtualListInfo = React.useMemo((): VirtualListInfo => ({
        scrollContainer: scrollContainer.current,
        listContainer: listContainer.current,
        rowHeights: rowHeights.current,
        rowOffsets: rowOffsets.current,
        value,
        rowsCount,
        blockSize,
        overdrawRows,
        listOffset,
        estimatedHeight,
        rowsSelector,
    }), [
        scrollContainer.current,
        listContainer.current,
        rowHeights.current,
        rowOffsets.current,
        value,
        rowsCount,
        blockSize,
        overdrawRows,
        listOffset,
        estimatedHeight,
        rowsSelector,
    ]);

    useLayoutEffectSafeForSsr(() => {
        if (__DEV__) {
            if (scrollContainer.current?.clientHeight
                && prevScrollContainerClientHeight
                && scrollContainer.current?.clientHeight !== prevScrollContainerClientHeight
            ) {
                ++scrollContainerHeightChangesCount.current;
            } else {
                scrollContainerHeightChangesCount.current = 0;
            }
            if (scrollContainerHeightChangesCount.current > 20 && !scrollContainerHeightIsNotLimited.current) {
                scrollContainerHeightIsNotLimited.current = true;
                devLogger.warn('[VirtualList]: The scroll container height is not limited. Please ensure that the VirtualList\'s parent container has a defined, limited height.');
            }
        }
    });

    useLayoutEffectSafeForSsr(() => {
        if (!scrollContainer.current || !listContainer.current) return;
        const { top: scrollContainerTop } = scrollContainer.current.getBoundingClientRect();
        const { top: listContainerTop } = listContainer.current.getBoundingClientRect();
        const newListOffset = listContainerTop - scrollContainerTop;
        setListOffset(newListOffset);
    }, [scrollContainer.current, listContainer.current]);

    const getTopIndexAndVisibleCountOnScroll = React.useCallback(() => {
        if (!virtualListInfo.scrollContainer || !virtualListInfo.value) {
            return {
                visibleCount: virtualListInfo.value?.visibleCount,
                topIndex: virtualListInfo.value?.topIndex,
            };
        }

        return getRowsToFetchForScroll(virtualListInfo);
    }, [virtualListInfo]);

    useLayoutEffectSafeForSsr(() => {
        const rowsInfo = getUpdatedRowsInfo(virtualListInfo);
        rowHeights.current = rowsInfo.rowHeights;
        rowOffsets.current = rowsInfo.rowOffsets;
        if (scrollContainer.current && value) onScroll?.(scrollContainer.current);
        if (!scrollContainer.current || !value) return;

        if (value?.scrollTo !== scrolledTo && value?.scrollTo?.index != null) {
            handleForceScrollToIndex(rowsInfo);
        } else {
            if (!scrollContainerHeightIsNotLimited.current) {
                handleScrollOnRerender(rowsInfo);
            }
        }
    });

    const handleForceScrollToIndex = (rowsInfo: RowsInfo) => {
        const assumedHeight = assumeHeightForScrollToIndex(value, rowsInfo.estimatedHeight, rowsInfo.averageRowHeight);
        const estimatedHeightToSet = rowsCount >= value.scrollTo.index
            ? rowsInfo.estimatedHeight
            : assumedHeight;
        setEstimatedHeight(estimatedHeightToSet);
        scrollToIndex(value.scrollTo);
    };

    const handleScrollOnRerender = (rowsInfo: RowsInfo) => {
        const { topIndex } = value;
        const { topIndex: newTopIndex, visibleCount } = getNewRowsOnScroll();

        if (estimatedHeight !== rowsInfo.estimatedHeight) {
            setEstimatedHeight(rowsInfo.estimatedHeight);
        }
        if (topIndex !== newTopIndex || visibleCount !== value.visibleCount) {
            onValueChange({ ...value, topIndex: newTopIndex, visibleCount });
        }
    };

    const getNewRowsOnScroll = React.useCallback(() => {
        const { topIndex, visibleCount } = getTopIndexAndVisibleCountOnScroll();
        if (topIndex !== value.topIndex || visibleCount > value.visibleCount) {
            return { topIndex, visibleCount };
        }

        return value;
    }, [getTopIndexAndVisibleCountOnScroll, onValueChange, value]);

    const scrollContainerToPosition = React.useCallback(
        (scrollTo: ScrollToConfig) => {
            const topCoordinate = getScrollToCoordinate(virtualListInfo, scrollTo);
            if (topCoordinate === undefined) {
                return [true, true]; // already at the necessary position, scroll doesn't have to be performed.
            }

            if (isNaN(topCoordinate)) {
                return [false, false];
            }

            scrollContainer.current.scrollTo({ top: topCoordinate, behavior: scrollTo.behavior });
            const scrollPositionDiff = (+topCoordinate.toFixed(0)) - (+scrollContainer.current.scrollTop.toFixed(0));

            return [
                scrollPositionDiff <= 1 // if scroll position is equal to expected one
                && virtualListInfo.rowHeights[scrollTo.index] !== undefined, // and required row with necessary index is present
                true,
            ];
        },
        [scrollContainer.current, rowOffsets.current, virtualListInfo],
    );

    const scrollToIndex = React.useCallback(
        (scrollTo: ScrollToConfig) => {
            const topIndex = getTopIndexWithOffset(scrollTo.index, overdrawRows, blockSize);
            const { visibleCount } = getTopIndexAndVisibleCountOnScroll();

            const [wasScrolled, ok] = scrollContainerToPosition(scrollTo);
            if ((ok && !wasScrolled) || value.topIndex !== topIndex || value.visibleCount !== visibleCount) {
                onValueChange({ ...value, topIndex, visibleCount, scrollTo });
            }

            const realTopIndex = getRealTopIndex(virtualListInfo);
            // prevents from cycling, while force scrolling to a row, which will never appear, when using LazyListView.
            const shouldScrollToUnknownIndex = value.topIndex === topIndex && value.scrollTo?.index > realTopIndex;
            if ((ok && wasScrolled) || shouldScrollToUnknownIndex) {
                if (value.scrollTo?.index === scrollTo.index) {
                    setScrolledTo(value.scrollTo);
                } else {
                    onValueChange({ ...value, scrollTo });
                    setScrolledTo(scrollTo);
                }
            }
        },
        [
            scrollContainer.current,
            rowOffsets.current,
            value?.topIndex,
            value?.scrollTo,
            overdrawRows,
            blockSize,
            scrollContainerToPosition,
            virtualListInfo,
        ],
    );

    const offsetY = React.useMemo(
        () => getOffsetYForIndex(value?.topIndex, rowOffsets.current, listOffset),
        [rowOffsets.current, listOffset, value?.topIndex],
    );

    const handleScroll = React.useCallback(() => {
        if (!scrollContainer.current && !value) return;
        onScroll?.(scrollContainer.current);

        if (value.scrollTo !== scrolledTo && value.scrollTo?.index != null) {
            return;
        }
        const newValue = getNewRowsOnScroll();
        if (value.topIndex !== newValue.topIndex || value.visibleCount !== newValue.visibleCount) {
            onValueChange({ ...value, ...newValue });
        }
    }, [value, onScroll, scrolledTo, scrollContainer.current, getNewRowsOnScroll]);

    return {
        estimatedHeight,
        offsetY,
        listOffset,

        scrollContainerRef: scrollContainer,
        listContainerRef: listContainer,

        handleScroll,
        scrollToIndex,
    };
}