| | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
| | import { throttle } from 'lodash'; |
| |
|
| | interface UseInfiniteScrollOptions { |
| | hasNextPage?: boolean; |
| | isLoading?: boolean; |
| | fetchNextPage: () => void; |
| | threshold?: number; |
| | throttleMs?: number; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export const useInfiniteScroll = ({ |
| | hasNextPage = false, |
| | isLoading = false, |
| | fetchNextPage, |
| | threshold = 0.8, // Trigger when 80% scrolled |
| | throttleMs = 200, |
| | }: UseInfiniteScrollOptions) => { |
| | |
| | const resizeObserverRef = useRef<ResizeObserver | null>(null); |
| | const [scrollElement, setScrollElementState] = useState<HTMLElement | null>(null); |
| |
|
| | |
| | const handleNeedToFetch = useCallback(() => { |
| | if (!scrollElement) return; |
| |
|
| | const { scrollTop, scrollHeight, clientHeight } = scrollElement; |
| |
|
| | |
| | const scrollPosition = (scrollTop + clientHeight) / scrollHeight; |
| |
|
| | |
| | const shouldFetch = scrollPosition >= threshold && hasNextPage && !isLoading; |
| |
|
| | if (shouldFetch) { |
| | fetchNextPage(); |
| | } |
| | }, [scrollElement, hasNextPage, isLoading, fetchNextPage, threshold]); |
| |
|
| | |
| | const throttledHandleNeedToFetch = useMemo( |
| | () => throttle(handleNeedToFetch, throttleMs), |
| | [handleNeedToFetch, throttleMs], |
| | ); |
| |
|
| | |
| | useEffect(() => { |
| | return () => { |
| | throttledHandleNeedToFetch.cancel?.(); |
| | }; |
| | }, [throttledHandleNeedToFetch]); |
| |
|
| | |
| | useEffect(() => { |
| | if (isLoading === false && scrollElement) { |
| | |
| | const rafId = requestAnimationFrame(() => { |
| | throttledHandleNeedToFetch(); |
| | }); |
| | return () => cancelAnimationFrame(rafId); |
| | } |
| | }, [isLoading, scrollElement, throttledHandleNeedToFetch]); |
| |
|
| | |
| | useEffect(() => { |
| | const element = scrollElement; |
| | if (!element) return; |
| |
|
| | |
| | element.addEventListener('scroll', throttledHandleNeedToFetch, { passive: true }); |
| |
|
| | |
| | if (resizeObserverRef.current) { |
| | resizeObserverRef.current.disconnect(); |
| | } |
| |
|
| | resizeObserverRef.current = new ResizeObserver(() => { |
| | |
| | throttledHandleNeedToFetch(); |
| | }); |
| |
|
| | resizeObserverRef.current.observe(element); |
| |
|
| | |
| | throttledHandleNeedToFetch(); |
| |
|
| | return () => { |
| | element.removeEventListener('scroll', throttledHandleNeedToFetch); |
| | |
| | if (resizeObserverRef.current) { |
| | resizeObserverRef.current.disconnect(); |
| | resizeObserverRef.current = null; |
| | } |
| | }; |
| | }, [scrollElement, throttledHandleNeedToFetch]); |
| |
|
| | |
| | const setScrollElement = useCallback((element: HTMLElement | null) => { |
| | setScrollElementState(element); |
| | }, []); |
| |
|
| | return { |
| | setScrollElement, |
| | }; |
| | }; |
| |
|