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
- Tạo API trả về dữ liệu phân trang (page, perPage)
- Hiển thị danh sách ban đầu
- Theo dõi scroll → nếu gần cuối trang → gọi API tiếp theo
- Gộp dữ liệu mới vào danh sách hiện tại
- 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
- Tạo component
<InfiniteBlog />
cho blog site - Test khi cuộn xuống: bài mới tự động hiện
- Làm version scroll trong container (not full page)
- Áp dụng cho danh sách bình luận hoặc sản phẩm
Thảo luận