File size: 3,599 Bytes
f0743f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { throttle } from 'lodash';

interface UseInfiniteScrollOptions {
  hasNextPage?: boolean;
  isLoading?: boolean;
  fetchNextPage: () => void;
  threshold?: number; // Percentage of scroll position to trigger fetch (0-1)
  throttleMs?: number; // Throttle delay in milliseconds
}

/**
 * Custom hook for implementing infinite scroll functionality
 * Detects when user scrolls near the bottom and triggers data fetching
 */
export const useInfiniteScroll = ({
  hasNextPage = false,
  isLoading = false,
  fetchNextPage,
  threshold = 0.8, // Trigger when 80% scrolled
  throttleMs = 200,
}: UseInfiniteScrollOptions) => {
  // Monitor resizing of the scroll container
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
  const [scrollElement, setScrollElementState] = useState<HTMLElement | null>(null);

  // Handler to check if we need to fetch more data
  const handleNeedToFetch = useCallback(() => {
    if (!scrollElement) return;

    const { scrollTop, scrollHeight, clientHeight } = scrollElement;

    // Calculate scroll position as percentage
    const scrollPosition = (scrollTop + clientHeight) / scrollHeight;

    // Check if we've scrolled past the threshold and conditions are met
    const shouldFetch = scrollPosition >= threshold && hasNextPage && !isLoading;

    if (shouldFetch) {
      fetchNextPage();
    }
  }, [scrollElement, hasNextPage, isLoading, fetchNextPage, threshold]);

  // Create a throttled version - using useMemo to ensure it's created synchronously
  const throttledHandleNeedToFetch = useMemo(
    () => throttle(handleNeedToFetch, throttleMs),
    [handleNeedToFetch, throttleMs],
  );

  // Clean up throttled function on unmount
  useEffect(() => {
    return () => {
      throttledHandleNeedToFetch.cancel?.();
    };
  }, [throttledHandleNeedToFetch]);

  // Check if we need to fetch more data when loading state changes (useful to fill content on first load)
  useEffect(() => {
    if (isLoading === false && scrollElement) {
      // Use requestAnimationFrame to ensure DOM is ready after loading completes
      const rafId = requestAnimationFrame(() => {
        throttledHandleNeedToFetch();
      });
      return () => cancelAnimationFrame(rafId);
    }
  }, [isLoading, scrollElement, throttledHandleNeedToFetch]);

  // Set up scroll listener and ResizeObserver
  useEffect(() => {
    const element = scrollElement;
    if (!element) return;

    // Add the scroll listener
    element.addEventListener('scroll', throttledHandleNeedToFetch, { passive: true });

    // Set up ResizeObserver to detect size changes
    if (resizeObserverRef.current) {
      resizeObserverRef.current.disconnect();
    }

    resizeObserverRef.current = new ResizeObserver(() => {
      // Check if we need to fetch more data when container resizes
      throttledHandleNeedToFetch();
    });

    resizeObserverRef.current.observe(element);

    // Check immediately when element changes
    throttledHandleNeedToFetch();

    return () => {
      element.removeEventListener('scroll', throttledHandleNeedToFetch);
      // Clean up ResizeObserver
      if (resizeObserverRef.current) {
        resizeObserverRef.current.disconnect();
        resizeObserverRef.current = null;
      }
    };
  }, [scrollElement, throttledHandleNeedToFetch]);

  // Function to manually set the scroll container
  const setScrollElement = useCallback((element: HTMLElement | null) => {
    setScrollElementState(element);
  }, []);

  return {
    setScrollElement,
  };
};