Intersection Observer detects when elements enter/exit the viewport, ideal for infinite scrolling.
Code:
1import { useState, useEffect, useRef, useCallback } from "react";23function InfiniteScroll() {4 const [items, setItems] = useState<Item[]>([]);5 const [loading, setLoading] = useState(false);6 const [hasMore, setHasMore] = useState(true);7 const observerRef = useRef<IntersectionObserver>();8 const sentinelRef = useRef<HTMLDivElement>(null);910 const loadMore = useCallback(async () => {11 if (loading || !hasMore) return;12 setLoading(true);1314 const newItems = await fetchItems(items.length);15 setItems(prev => [...prev, ...newItems]);16 setHasMore(newItems.length > 0);17 setLoading(false);18 }, [items.length, loading, hasMore]);1920 useEffect(() => {21 observerRef.current = new IntersectionObserver(22 (entries) => {23 if (entries[0].isIntersecting) {24 loadMore();25 }26 },27 { threshold: 0.1, rootMargin: "100px" }28 );2930 if (sentinelRef.current) {31 observerRef.current.observe(sentinelRef.current);32 }3334 return () => observerRef.current?.disconnect();35 }, [loadMore]);3637 return (38 <div>39 {items.map(item => (40 <div key={item.id} className="item">41 {item.title}42 </div>43 ))}4445 {/* Sentinel element */}46 <div ref={sentinelRef} style={{ height: 1 }} />4748 {loading && <div className="spinner">Loading...</div>}49 </div>50 );51}
Reusable hook:
1function useInfiniteScroll(loadMore: () => Promise<void>, hasMore: boolean) {2 const observerRef = useRef<IntersectionObserver>();3 const sentinelRef = useCallback((node: HTMLDivElement | null) => {4 if (observerRef.current) observerRef.current.disconnect();5 if (!node || !hasMore) return;67 observerRef.current = new IntersectionObserver((entries) => {8 if (entries[0].isIntersecting) loadMore();9 });10 observerRef.current.observe(node);11 }, [loadMore, hasMore]);1213 return sentinelRef;14}1516// Usage17const sentinelRef = useInfiniteScroll(fetchNextPage, hasNextPage);18<div ref={sentinelRef} />