0
0
VueHow-ToBeginner · 4 min read

How to Create Infinite Scroll in Vue: Simple Guide

To create infinite scroll in Vue, use the Intersection Observer API to detect when the user scrolls near the bottom and then load more data. Combine this with Vue's ref and watchEffect to update the list dynamically as new items load.
📐

Syntax

Infinite scroll in Vue typically uses the Intersection Observer API to watch a sentinel element at the bottom of the list. When this element enters the viewport, a callback triggers loading more data. Vue's ref holds reactive data, and onMounted sets up the observer.

  • ref: reactive variable for list and loading state
  • onMounted: lifecycle hook to start observer
  • IntersectionObserver: watches the sentinel element
  • loadMore: function to fetch and append data
vue
import { ref, onMounted } from 'vue';

export default {
  setup() {
    const items = ref([]);
    const loading = ref(false);
    const page = ref(1);

    const loadMore = async () => {
      if (loading.value) return;
      loading.value = true;
      // fetch data here (e.g., API call)
      const newItems = await fetchData(page.value);
      items.value.push(...newItems);
      page.value++;
      loading.value = false;
    };

    onMounted(() => {
      const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) {
          loadMore();
        }
      });
      observer.observe(document.querySelector('#sentinel'));
    });

    return { items, loading };
  }
};

async function fetchData(page) {
  // Simulate API delay and data
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(Array.from({ length: 10 }, (_, i) => `Item ${(page - 1) * 10 + i + 1}`));
    }, 500);
  });
}
💻

Example

This example shows a Vue 3 component that loads more items when you scroll to the bottom. It uses the Intersection Observer to detect when the sentinel div is visible and then fetches more items asynchronously.

vue
<template>
  <main>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
    <div id="sentinel" aria-label="Loading more items" style="height: 1rem;"></div>
    <p v-if="loading">Loading...</p>
  </main>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const items = ref([]);
const loading = ref(false);
const page = ref(0);

const loadMore = async () => {
  if (loading.value) return;
  loading.value = true;
  page.value++;
  const newItems = await fetchData(page.value);
  items.value.push(...newItems);
  loading.value = false;
};

onMounted(() => {
  loadMore(); // initial load
  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      loadMore();
    }
  });
  observer.observe(document.getElementById('sentinel'));
});

async function fetchData(page) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(Array.from({ length: 10 }, (_, i) => `Item ${(page - 1) * 10 + i + 1}`));
    }, 500);
  });
}
</script>

<style scoped>
main {
  max-width: 400px;
  margin: 2rem auto;
  font-family: Arial, sans-serif;
}
ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
li {
  padding: 0.5rem;
  border-bottom: 1px solid #ddd;
}
#sentinel {
  background: transparent;
}
p {
  text-align: center;
  color: #666;
}
</style>
Output
A scrollable list showing items labeled 'Item 1' to 'Item 10' and so on, loading more as you scroll down, with a 'Loading...' text briefly visible during data fetch.
⚠️

Common Pitfalls

Common mistakes when implementing infinite scroll in Vue include:

  • Not disconnecting the IntersectionObserver when the component unmounts, causing memory leaks.
  • Triggering multiple loads at once by not checking if a load is already in progress.
  • Not handling the case when no more data is available, causing endless loading.
  • Using a sentinel element that is not visible or too small to trigger the observer.

Always check loading state before fetching more data and disconnect the observer in onUnmounted.

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

export default {
  setup() {
    const items = ref([]);
    const loading = ref(false);
    const page = ref(1);
    let observer = null;

    const loadMore = async () => {
      if (loading.value) return; // prevent multiple calls
      loading.value = true;
      const newItems = await fetchData(page.value);
      if (newItems.length === 0) {
        observer.disconnect(); // no more data
        return;
      }
      items.value.push(...newItems);
      page.value++;
      loading.value = false;
    };

    onMounted(() => {
      observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) {
          loadMore();
        }
      });
      observer.observe(document.querySelector('#sentinel'));
    });

    onUnmounted(() => {
      if (observer) observer.disconnect();
    });

    return { items, loading };
  }
};

async function fetchData(page) {
  // Simulate API call
  return new Promise(resolve => {
    setTimeout(() => {
      if (page > 5) resolve([]); // no more data after 5 pages
      else resolve(Array.from({ length: 10 }, (_, i) => `Item ${(page - 1) * 10 + i + 1}`));
    }, 500);
  });
}
📊

Quick Reference

  • Use IntersectionObserver to detect scroll near bottom.
  • Keep track of loading state to avoid duplicate fetches.
  • Disconnect observer on component unmount to prevent leaks.
  • Handle end of data to stop infinite loading.
  • Use a sentinel element with visible height at the list bottom.

Key Takeaways

Use Intersection Observer to detect when to load more items in Vue infinite scroll.
Always track loading state to prevent multiple simultaneous data fetches.
Disconnect the observer when the component unmounts to avoid memory leaks.
Handle the end of data gracefully to stop infinite loading.
Place a sentinel element at the bottom of the list to trigger loading.