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 stateonMounted: lifecycle hook to start observerIntersectionObserver: watches the sentinel elementloadMore: 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
IntersectionObserverwhen 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
IntersectionObserverto 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.