JAVASCRIPT

Build an Infinite Scroll Composable in Vue 3

Create a Vue 3 composable (`useInfiniteScroll`) to easily implement infinite scrolling for lists, efficiently loading more data as the user scrolls.

// composables/useInfiniteScroll.js
import { ref, onMounted, onUnmounted, nextTick } from 'vue';

export function useInfiniteScroll(loadMoreCallback, options = {}) {
  const isLoading = ref(false);
  const hasMore = ref(true); // Assume there's more data initially
  const scrollContainer = ref(options.container || window); // Default to window

  const threshold = options.threshold || 100; // Distance from bottom to trigger load

  const handleScroll = async () => {
    if (!hasMore.value || isLoading.value) {
      return;
    }

    let isNearBottom = false;
    if (scrollContainer.value === window) {
      const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
      isNearBottom = scrollTop + clientHeight >= scrollHeight - threshold;
    } else if (scrollContainer.value) {
      const { scrollTop, scrollHeight, clientHeight } = scrollContainer.value;
      isNearBottom = scrollTop + clientHeight >= scrollHeight - threshold;
    }

    if (isNearBottom) {
      isLoading.value = true;
      try {
        const moreDataAvailable = await loadMoreCallback(); // Callback should return true if more data, false if not
        hasMore.value = moreDataAvailable !== false; // If callback explicitly returns false, set hasMore to false
      } catch (error) {
        console.error('Error loading more data:', error);
        hasMore.value = false; // Stop trying if there's an error
      } finally {
        isLoading.value = false;
      }
    }
  };

  onMounted(() => {
    nextTick(() => { // Ensure container is rendered before attaching listener
      if (scrollContainer.value) {
        scrollContainer.value.addEventListener('scroll', handleScroll);
        handleScroll(); // Initial check in case content is short
      }
    });
  });

  onUnmounted(() => {
    if (scrollContainer.value) {
      scrollContainer.value.removeEventListener('scroll', handleScroll);
    }
  });

  return {
    isLoading,
    hasMore,
    scrollContainer, // Expose to allow setting container ref from parent
  };
}

// In your component:
/*
<template>
  <div :ref="setContainerRef" class="scroll-container" style="max-height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px;">
    <div v-for="item in items" :key="item.id" class="item">
      {{ item.text }}
    </div>
    <div v-if="isLoading" class="loading-indicator">Loading more...</div>
    <div v-if="!hasMore && !isLoading" class="no-more-data">No more items.</div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { useInfiniteScroll } from '@/composables/useInfiniteScroll'; // Adjust path

const items = ref([]);
const page = ref(1);
const totalItems = 30; // Simulate total available items
const itemsPerPage = 5;

// Simulate API call
const fetchItems = async () => {
  return new Promise(resolve => {
    setTimeout(() => {
      const start = (page.value - 1) * itemsPerPage;
      const end = start + itemsPerPage;
      const newItems = [];
      for (let i = start; i < end && i < totalItems; i++) {
        newItems.push({ id: i + 1, text: `Item ${i + 1}` });
      }
      resolve(newItems);
    }, 500); // Simulate network delay
  });
};

const loadMore = async () => {
  if (items.value.length >= totalItems) {
    return false; // No more data
  }

  const newItems = await fetchItems();
  items.value = [...items.value, ...newItems];
  page.value++;
  return newItems.length > 0; // Return true if new items were loaded
};

const { isLoading, hasMore, scrollContainer } = useInfiniteScroll(loadMore, { threshold: 200 });

const setContainerRef = (el) => {
  scrollContainer.value = el; // Assign the DOM element to the composable's ref
};

// Initial load
onMounted(async () => {
  await loadMore();
});
</script>

<style scoped>
.scroll-container {
  margin-top: 20px;
  padding: 10px;
  background-color: #f9f9f9;
}
.item {
  padding: 10px 0;
  border-bottom: 1px dashed #eee;
}
.item:last-child {
  border-bottom: none;
}
.loading-indicator, .no-more-data {
  text-align: center;
  padding: 15px;
  color: #777;
}
</style>
*/
How it works: This `useInfiniteScroll` composable simplifies implementing an infinite scrolling mechanism in Vue 3. It takes a `loadMoreCallback` function (which should return `true` if more data is available or `false` otherwise) and options like a scroll `container` (defaults to `window`) and a `threshold` distance from the bottom. It attaches a scroll event listener to the specified container, triggering the `loadMoreCallback` when the user scrolls near the bottom. `isLoading` and `hasMore` refs provide feedback on the loading state and data availability, preventing redundant calls and indicating when all data has been fetched.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs