import { ReactNode, useEffect, useRef, useState } from "react"; import { Box } from "@mantine/core"; interface InfiniteScrollProps { items: T[]; renderItem: (item: T, index: number) => ReactNode; batchSize?: number; initialCount?: number; rootMargin?: string; loader?: ReactNode; hasMore?: boolean; loading?: boolean; onLoadMore?: () => void; } const InfiniteScroll = ({ items, renderItem, batchSize = 25, initialCount = batchSize, rootMargin = "600px 0px", loader, hasMore = false, loading = false, onLoadMore, }: InfiniteScrollProps) => { const [visibleCount, setVisibleCount] = useState(initialCount); const [prevItems, setPrevItems] = useState(items); const sentinelRef = useRef(null); if (prevItems !== items) { setPrevItems(items); setVisibleCount(initialCount); } const hasHiddenItems = visibleCount < items.length; const showLoader = hasHiddenItems || hasMore || loading; useEffect(() => { const sentinel = sentinelRef.current; if (!sentinel) return; const observer = new IntersectionObserver( (entries) => { if (!entries.some((entry) => entry.isIntersecting)) return; if (visibleCount < items.length) { setVisibleCount((count) => Math.min(count + batchSize, items.length)); } else if (hasMore && !loading) { onLoadMore?.(); } }, { rootMargin } ); observer.observe(sentinel); return () => observer.disconnect(); }, [items, visibleCount, batchSize, hasMore, loading, onLoadMore, rootMargin]); return ( <> {items.slice(0, visibleCount).map((item, index) => renderItem(item, index))} {showLoader && ( <> {loader} )} ); }; export default InfiniteScroll;