From 049d23685f9c60b6f38e4c2ae59da4b66ed3e0cc Mon Sep 17 00:00:00 2001 From: Abhishek Tomar <104965815+abhisektomar1@users.noreply.github.com> Date: Sat, 13 Jul 2024 00:05:37 +0530 Subject: [PATCH] fix(table): resolve double fetch issue in useInfiniteScroll hook (#3332) * fix(table): resolve double fetch issue in useInfiniteScroll hook (fix #3251) * fix(table): remove unnecessary else clause * fix(table): add a changeset file for use-infinite-scroll * fix(hooks): add clearTimeout function --------- Co-authored-by: WK Wong --- .changeset/fix-infinite-scroll.md | 5 ++ .../hooks/use-infinite-scroll/src/index.ts | 87 +++++++++++-------- 2 files changed, 55 insertions(+), 37 deletions(-) create mode 100644 .changeset/fix-infinite-scroll.md diff --git a/.changeset/fix-infinite-scroll.md b/.changeset/fix-infinite-scroll.md new file mode 100644 index 0000000000..31645ad548 --- /dev/null +++ b/.changeset/fix-infinite-scroll.md @@ -0,0 +1,5 @@ +--- +"@nextui-org/use-infinite-scroll": patch +--- + +fix(table): resolve double fetch issue in useInfiniteScroll hook (#3251) \ No newline at end of file diff --git a/packages/hooks/use-infinite-scroll/src/index.ts b/packages/hooks/use-infinite-scroll/src/index.ts index 7c9f0542ae..5c897fd063 100644 --- a/packages/hooks/use-infinite-scroll/src/index.ts +++ b/packages/hooks/use-infinite-scroll/src/index.ts @@ -1,5 +1,5 @@ import debounce from "lodash.debounce"; -import {useLayoutEffect, useRef} from "react"; +import {useLayoutEffect, useRef, useCallback} from "react"; export interface UseInfiniteScrollProps { /** @@ -27,13 +27,32 @@ export interface UseInfiniteScrollProps { } export function useInfiniteScroll(props: UseInfiniteScrollProps = {}) { - const {hasMore, distance = 250, isEnabled = true, shouldUseLoader = true, onLoadMore} = props; + const { + hasMore = true, + distance = 250, + isEnabled = true, + shouldUseLoader = true, + onLoadMore, + } = props; const scrollContainerRef = useRef(null); const loaderRef = useRef(null); + const observerRef = useRef(null); + const isLoadingRef = useRef(false); + + const loadMore = useCallback(() => { + let timer: ReturnType; + + if (!isLoadingRef.current && hasMore && onLoadMore) { + isLoadingRef.current = true; + onLoadMore(); + timer = setTimeout(() => { + isLoadingRef.current = false; + }, 100); // Debounce time to prevent multiple calls + } - const previousY = useRef(); - const previousRatio = useRef(0); + return () => clearTimeout(timer); + }, [hasMore, onLoadMore]); useLayoutEffect(() => { const scrollContainerNode = scrollContainerRef.current; @@ -48,50 +67,44 @@ export function useInfiniteScroll(props: UseInfiniteScrollProps = {}) { const options = { root: scrollContainerNode, rootMargin: `0px 0px ${distance}px 0px`, + threshold: 0.1, }; - const listener = (entries: IntersectionObserverEntry[]) => { - entries.forEach(({isIntersecting, intersectionRatio, boundingClientRect = {}}) => { - const y = boundingClientRect.y || 0; - - if ( - isIntersecting && - intersectionRatio >= previousRatio.current && - (!previousY.current || y < previousY.current) - ) { - onLoadMore?.(); - } - previousY.current = y; - previousRatio.current = intersectionRatio; - }); - }; + const observer = new IntersectionObserver((entries) => { + const [entry] = entries; - const observer = new IntersectionObserver(listener, options); + if (entry.isIntersecting) { + loadMore(); + } + }, options); observer.observe(loaderNode); + observerRef.current = observer; - return () => observer.disconnect(); - } else { - const debouncedOnLoadMore = onLoadMore ? debounce(onLoadMore, 200) : undefined; - - const checkIfNearBottom = () => { - if ( - scrollContainerNode.scrollHeight - scrollContainerNode.scrollTop <= - scrollContainerNode.clientHeight + distance - ) { - debouncedOnLoadMore?.(); + return () => { + if (observerRef.current) { + observerRef.current.disconnect(); } }; + } - scrollContainerNode.addEventListener("scroll", checkIfNearBottom); + const debouncedCheckIfNearBottom = debounce(() => { + if ( + scrollContainerNode.scrollHeight - scrollContainerNode.scrollTop <= + scrollContainerNode.clientHeight + distance + ) { + loadMore(); + } + }, 100); - return () => { - scrollContainerNode.removeEventListener("scroll", checkIfNearBottom); - }; - } - }, [hasMore, distance, isEnabled, onLoadMore, shouldUseLoader]); + scrollContainerNode.addEventListener("scroll", debouncedCheckIfNearBottom); + + return () => { + scrollContainerNode.removeEventListener("scroll", debouncedCheckIfNearBottom); + }; + }, [hasMore, distance, isEnabled, shouldUseLoader, loadMore]); - return [loaderRef, scrollContainerRef]; + return [loaderRef, scrollContainerRef] as const; } export type UseInfiniteScrollReturn = ReturnType;