Đăng nhập

BÀI 26: INFINITE SCROLL & LOAD ON SCROLL TRONG NUXT

Cùng tiếp tục với Bài 26 – Tạo component dạng “load on scroll” và infinite scroll trong NuxtJS. Đây là tính năng phổ biến trong blog, diễn đàn, trang tin tức hoặc ứng dụng mạng xã hội – nơi người dùng kéo xuống để xem thêm mà không cần chuyển trang.


1. Infinite scroll là gì?

  • Là kỹ thuật tải thêm dữ liệu khi cuộn đến cuối trang
  • Thay vì nhấn “Trang 2”, bạn chỉ cần cuộn xuống để tự động hiển thị thêm nội dung
  • Tăng trải nghiệm người dùng, đặc biệt cho thiết bị di động

2. Các bước cơ bản để làm infinite scroll

  1. Tạo API trả về dữ liệu phân trang (page, perPage)
  2. Hiển thị danh sách ban đầu
  3. Theo dõi scroll → nếu gần cuối trang → gọi API tiếp theo
  4. Gộp dữ liệu mới vào danh sách hiện tại
  5. Hiển thị loading indicator

3. Ví dụ: Tải danh sách bài viết dạng infinite scroll

a) Giả sử API bạn gọi:

GET /api/posts?page=1&limit=5

b) Tạo component: components/PostList.vue

<template>
  <div ref="container">
    <div v-for="post in posts" :key="post.id" class="post-item">
      <h3>{{ post.title }}</h3>
      <p>{{ post.excerpt }}</p>
    </div>
    <p v-if="loading">Đang tải thêm...</p>
    <p v-if="noMore">Hết nội dung!</p>
  </div>
</template>

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

const posts = ref([])
const page = ref(1)
const limit = 5
const loading = ref(false)
const noMore = ref(false)

async function loadMore() {
  if (loading.value || noMore.value) return

  loading.value = true
  const newPosts = await $fetch(`/api/posts?page=${page.value}&limit=${limit}`)

  if (newPosts.length < limit) noMore.value = true
  posts.value.push(...newPosts)
  page.value++
  loading.value = false
}

// Infinite scroll theo scroll toàn trang
function handleScroll() {
  const nearBottom =
    window.innerHeight + window.scrollY >= document.body.offsetHeight - 200
  if (nearBottom) loadMore()
}

onMounted(() => {
  loadMore()
  window.addEventListener('scroll', handleScroll)
})
</script>

4. Hoặc dùng IntersectionObserver (hiệu quả hơn)

Tạo element “bottom trigger”:

<div ref="bottomTrigger"></div>

Trong script:

const bottomTrigger = ref(null)

onMounted(() => {
  loadMore()

  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) loadMore()
  }, { rootMargin: '100px' })

  if (bottomTrigger.value) observer.observe(bottomTrigger.value)
})

Không cần tính toán scrollY, hiệu quả hơn và ít lỗi.


5. Kết hợp với useAsyncData() hoặc API server

Nếu bạn dùng useFetch() hoặc useAsyncData() để gọi API, vẫn dùng tương tự:

const { data: posts } = await useAsyncData('posts', () =>
  $fetch('/api/posts?page=1&limit=5')
)

Sau đó dùng push(...newData) khi load thêm.


6. Gợi ý style cho trải nghiệm mượt mà

.post-item {
  padding: 1rem;
  border-bottom: 1px solid #ddd;
}

7. Bonus: Dùng thư viện vueuse/core với useIntersectionObserver

npm install @vueuse/core
import { useIntersectionObserver } from '@vueuse/core'

useIntersectionObserver(bottomTrigger, ([entry]) => {
  if (entry.isIntersecting) loadMore()
})

Bạn đã học được

  • Cách xây dựng infinite scroll thủ công hoặc dùng IntersectionObserver
  • Load dữ liệu từng trang → gộp vào danh sách hiện có
  • Hiển thị loading / thông báo hết nội dung
  • Tối ưu performance bằng thư viện @vueuse/core

Bài tập mở rộng

  1. Tạo component <InfiniteBlog /> cho blog site
  2. Test khi cuộn xuống: bài mới tự động hiện
  3. Làm version scroll trong container (not full page)
  4. Áp dụng cho danh sách bình luận hoặc sản phẩm

Thảo luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Đăng ký nhận tin mới

Nhận bài học, tài nguyên và cơ hội việc làm qua email hàng tuần.

Chúng tôi cam kết không spam. Bạn có thể hủy bất cứ lúc nào.