Simple infinite scroll component. You have fully control over the loading spinner and IntersectionObserver API.
1import * as React from "react";
2
3interface InfiniteScrollProps {
4 isLoading: boolean;
5 hasMore: boolean;
6 next: () => unknown;
7 threshold?: number;
8 root?: Element | Document | null;
9 rootMargin?: string;
10 reverse?: boolean;
11 children?: React.ReactNode;
12}
13
14export default function InfiniteScroll({
15 isLoading,
16 hasMore,
17 next,
18 threshold = 1,
19 root = null,
20 rootMargin = "0px",
21 reverse,
22 children,
23}: InfiniteScrollProps) {
24 const observer = React.useRef<IntersectionObserver | null>(null);
25 // This callback ref will be called when it is dispatched to an element or detached from an element,
26 // or when the callback function changes.
27 const observerRef = React.useCallback(
28 (element: HTMLElement | null) => {
29 let safeThreshold = threshold;
30 if (threshold < 0 || threshold > 1) {
31 console.warn(
32 "threshold should be between 0 and 1. You are exceed the range. will use default value: 1",
33 );
34 safeThreshold = 1;
35 }
36 // When isLoading is true, this callback will do nothing.
37 // It means that the next function will never be called.
38 // It is safe because the intersection observer has disconnected the previous element.
39 if (isLoading) return;
40
41 if (observer.current) observer.current.disconnect();
42 if (!element) return;
43
44 // Create a new IntersectionObserver instance because hasMore or next may be changed.
45 observer.current = new IntersectionObserver(
46 (entries) => {
47 if (entries[0].isIntersecting && hasMore) {
48 next();
49 }
50 },
51 { threshold: safeThreshold, root, rootMargin },
52 );
53 observer.current.observe(element);
54 },
55 [hasMore, isLoading, next, threshold, root, rootMargin],
56 );
57
58 const flattenChildren = React.useMemo(
59 () => React.Children.toArray(children),
60 [children],
61 );
62
63 return (
64 <>
65 {flattenChildren.map((child, index) => {
66 if (!React.isValidElement(child)) {
67 process.env.NODE_ENV === "development" &&
68 console.warn("You should use a valid element with InfiniteScroll");
69 return child;
70 }
71
72 const isObserveTarget = reverse
73 ? index === 0
74 : index === flattenChildren.length - 1;
75 const ref = isObserveTarget ? observerRef : null;
76 // @ts-ignore ignore ref type
77 return React.cloneElement(child, { ref });
78 })}
79 </>
80 );
81}
82