VUE

useInfiniteScroll Composable

Composable for infinite scroll functionality with intersection observer

Vue3ComposablesInfiniteScrollIntersectionObserverUI

Code

import { ref, onMounted, onUnmounted, type Ref } from 'vue';

interface UseInfiniteScrollOptions {
  root?: HTMLElement | null;
  rootMargin?: string;
  threshold?: number;
  hasMore: Ref<boolean>;
  loadMore: () => Promise<void>;
}

export function useInfiniteScroll(
  target: Ref<HTMLElement | null>,
  options: UseInfiniteScrollOptions
) {
  const loading = ref(false);
  let observer: IntersectionObserver | null = null;

  const handleIntersect = __TOKEN_23__ ([entry]: IntersectionObserverEntry[]) => {
    __TOKEN_24__ (entry.isIntersecting && options.hasMore.value && !loading.value) {
      loading.value = true;
      try {
        await options.loadMore();
      } finally {
        loading.value = false;
      }
    }
  };

  onMounted(() => {
    __TOKEN_28__ (!target.value || !IntersectionObserver) return;

    observer = new IntersectionObserver(handleIntersect, {
      root: options.root,
      rootMargin: options.rootMargin || '200px',
      threshold: options.threshold || 0
    });

    observer.observe(target.value);
  });

  onUnmounted(() => {
    __TOKEN_31__ (observer && target.value) {
      observer.unobserve(target.value);
      observer.disconnect();
    }
  });

  return { loading };
}

// Usage example
// const posts = ref([]);
// const page = ref(1);
// const hasMore = ref(true);
// const loadMorePosts = async () => {
//   const newPosts = await fetch(`/api/posts?page=${page.value}`).then(r => r.json());
//   if (newPosts.length === 0) hasMore.value = false;
//   posts.value.push(...newPosts);
//   page.value++;
// };
// const loadMoreRef = ref<HTMLElement | null>(null);
// const { loading } = useInfiniteScroll(loadMoreRef, { hasMore, loadMore: loadMorePosts });