Skip to main content
A hook for implementing infinite scroll loading behavior using Intersection Observer.

Usage

import { useInfiniteScroll } from "@servicetitan/ext-atlas";

function InfiniteList() {
  const [items, setItems] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const [isLoading, setIsLoading] = useState(false);

  const loadMore = async () => {
    setIsLoading(true);
    const newItems = await fetchItems();
    setItems((prev) => [...prev, ...newItems]);
    setHasMore(newItems.length > 0);
    setIsLoading(false);
  };

  const { sentinelRef } = useInfiniteScroll({
    hasMore,
    isLoading,
    onLoadMore: loadMore,
    rootMargin: "100px",
    threshold: 0.1,
  });

  return (
    <div>
      {items.map((item) => (
        <Item key={item.id} {...item} />
      ))}
      {hasMore && <div ref={sentinelRef} />}
    </div>
  );
}

Options

OptionTypeDefaultDescription
hasMoreboolean-Whether there is more content to load
isLoadingboolean-Whether content is currently being loaded
onLoadMore() => void-Callback to load more content
rootMarginstring”100px”Margin around the root for intersection calculation
thresholdnumber0.1Visibility threshold to trigger loading

Returns

PropertyTypeDescription
sentinelRefRefObject<HTMLDivElement>Ref to attach to the sentinel element

Behavior

  • Intersection Observer: Uses native browser API for efficient scroll detection
  • Load Gating: Only triggers onLoadMore when hasMore is true and isLoading is false
  • Cleanup: Properly disconnects observer on unmount or option changes

Example: With InfiniteContent Component

The InfiniteContent component uses this hook internally, but you can use the hook directly for custom implementations:
import { useInfiniteScroll, Spinner } from "@servicetitan/ext-atlas";

function CustomInfiniteScroll() {
  const [messages, setMessages] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);

  const { sentinelRef } = useInfiniteScroll({
    hasMore,
    isLoading: loading,
    onLoadMore: async () => {
      setLoading(true);
      const older = await fetchOlderMessages();
      setMessages((prev) => [...older, ...prev]);
      setHasMore(older.length === 20);
      setLoading(false);
    },
    rootMargin: "200px",
  });

  return (
    <div className="message-list">
      {hasMore && <div ref={sentinelRef}>{loading && <Spinner />}</div>}
      {messages.map((msg) => (
        <Message key={msg.id} {...msg} />
      ))}
    </div>
  );
}
Last modified on February 3, 2026