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.

Create a free account to save this page, take notes and track your progress. Sign in

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 useInfiniteScroll wraps 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

Was this page helpful?