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.