typescriptintermediate

Infinite Scroll Hook v2

Intersection Observer-based infinite scroll with loading, error, and hasMore states.

typescript
import { useState, useEffect, useRef, useCallback } from 'react';

interface UseInfiniteScrollOptions<T> {
  fetchFn: (page: number) => Promise<T[]>;
  pageSize?: number;
}

export function useInfiniteScroll<T>({ fetchFn, pageSize = 20 }: UseInfiniteScrollOptions<T>) {
  const [items, setItems] = useState<T[]>([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const observerRef = useRef<IntersectionObserver>();

  const loadMore = useCallback(async () => {
    if (loading || !hasMore) return;
    setLoading(true);
    try {
      const newItems = await fetchFn(page);
      setItems(prev => [...prev, ...newItems]);
      setHasMore(newItems.length === pageSize);
      setPage(p => p + 1);
    } finally {
      setLoading(false);
    }
  }, [page, loading, hasMore, fetchFn, pageSize]);

  const sentinelRef = useCallback((node: HTMLElement | null) => {
    if (observerRef.current) observerRef.current.disconnect();
    observerRef.current = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting) loadMore();
    });
    if (node) observerRef.current.observe(node);
  }, [loadMore]);

  return { items, loading, hasMore, sentinelRef };
}

Use Cases

  • Feed-style content
  • Search results pagination

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.