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
| Option | Type | Default | Description |
|---|
| hasMore | boolean | - | Whether there is more content to load |
| isLoading | boolean | - | Whether content is currently being loaded |
| onLoadMore | () => void | - | Callback to load more content |
| rootMargin | string | ”100px” | Margin around the root for intersection calculation |
| threshold | number | 0.1 | Visibility threshold to trigger loading |
Returns
| Property | Type | Description |
|---|
| sentinelRef | RefObject<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