Vue cookbook · Intermediate
Load more items with infinite scroll
A long list should load the next page as users approach the end, without scroll listeners.
Approach
Observe a sentinel element at the end of the list with IntersectionObserver, load the next page when it becomes
visible, stop when there are no more pages, and disconnect the observer on unmount.
Solution
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, useTemplateRef } from "vue";
type Post = { id: number; title: string };
const posts = ref<Post[]>([]);
const cursor = ref<string | null>(null);
const done = ref(false);
const loading = ref(false);
const sentinel = useTemplateRef<HTMLElement>("sentinel");
let observer: IntersectionObserver | undefined;
async function loadMore() {
if (loading.value || done.value) return;
loading.value = true;
const res = await fetch(`/api/posts?cursor=${cursor.value ?? ""}`);
const page: { items: Post[]; next: string | null } = await res.json();
posts.value.push(...page.items);
cursor.value = page.next;
done.value = page.next === null;
loading.value = false;
}
onMounted(() => {
observer = new IntersectionObserver((entries) => {
if (entries[0]?.isIntersecting) loadMore();
}, { rootMargin: "400px" }); // start loading before the end is visible
if (sentinel.value) observer.observe(sentinel.value);
});
onBeforeUnmount(() => observer?.disconnect());
</script>
<template>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
<div ref="sentinel" aria-hidden="true"></div>
<p v-if="loading">Loading more...</p>
<p v-else-if="done">You've reached the end.</p>
</template>
Pitfalls
- Offset pagination on lists that change, causing duplicates or skipped items; use cursors.
- No "load more" button fallback for keyboard users and when loading fails.
- Rendering thousands of DOM nodes; combine with virtual scrolling for very long lists.
Variations
- VueUse's
useInfiniteScrollwraps the same pattern. - Keep the loaded pages in a store so returning from a detail page restores the list.
Further reading: useTemplateRef
Reference: https://vuejs.org/api/composition-api-helpers.html#usetemplateref
More Vue recipes
- Access child component methods with template refs and defineExpose
- Add SEO metadata and structured data in Nuxt
- Build an accessible modal with Teleport
- Build custom inputs with defineModel and modifiers
- Catch component errors with an error boundary
- Debounce user input with a custom ref
- Fetch data in a composable and cancel stale requests
- Fetch, refresh and share data with Nuxt useFetch