Skip to content

Commit

Permalink
fix(table): resolve double fetch issue in useInfiniteScroll hook (#3332)
Browse files Browse the repository at this point in the history
* 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 <wingkwong.code@gmail.com>
  • Loading branch information
abhisektomar1 and wingkwong authored Jul 12, 2024
1 parent 6ec494a commit 049d236
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/fix-infinite-scroll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/use-infinite-scroll": patch
---

fix(table): resolve double fetch issue in useInfiniteScroll hook (#3251)
87 changes: 50 additions & 37 deletions packages/hooks/use-infinite-scroll/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import debounce from "lodash.debounce";
import {useLayoutEffect, useRef} from "react";
import {useLayoutEffect, useRef, useCallback} from "react";

export interface UseInfiniteScrollProps {
/**
Expand Down Expand Up @@ -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<HTMLElement>(null);
const loaderRef = useRef<HTMLElement>(null);
const observerRef = useRef<IntersectionObserver | null>(null);
const isLoadingRef = useRef(false);

const loadMore = useCallback(() => {
let timer: ReturnType<typeof setTimeout>;

if (!isLoadingRef.current && hasMore && onLoadMore) {
isLoadingRef.current = true;
onLoadMore();
timer = setTimeout(() => {
isLoadingRef.current = false;
}, 100); // Debounce time to prevent multiple calls
}

const previousY = useRef<number>();
const previousRatio = useRef<number>(0);
return () => clearTimeout(timer);
}, [hasMore, onLoadMore]);

useLayoutEffect(() => {
const scrollContainerNode = scrollContainerRef.current;
Expand All @@ -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<typeof useInfiniteScroll>;

0 comments on commit 049d236

Please sign in to comment.