Query Odaklı Infinite Scroll — Vue.js
Selamlar! Modern web uygulamalarının vazgeçilmez bir parçası olan infinite scroll, kullanıcı deneyimini akıcı hale getirse de çoğu zaman…
Query Odaklı Infinite Scroll — Vue.js
Selamlar! Modern web uygulamalarının vazgeçilmez bir parçası olan infinite scroll, kullanıcı deneyimini akıcı hale getirse de çoğu zaman önemli bir problemi beraberinde getirir: kaybolan state. Kullanıcı sayfayı yenilediğinde veya bir linki paylaştığında, kaydırdığı onca içeriğin ortasındaki konumunu kaybeder.
Bu yazıda, bu problemi ortadan kaldıran, sayfa durumunu URL’e yansıtan, hem aşağı hem de yukarı yönde çalışan, gelişmiş bir infinite scroll bileşenini Vue 3, Pinia ve VueUse ile nasıl oluşturacağımızı adım adım inceleyeceğiz.
Hedefimiz Ne? 🎯
- Kullanıcı sayfayı aşağı kaydırdıkça yeni içerikler yüklenecek.
- Kullanıcı sayfayı yukarı kaydırdıkça (belirli bir etkileşimden sonra) eski içerikler yüklenecek.
- O an görüntülenen içeriklerin sayfa numarası her zaman URL’de (
?page=5gibi) görünecek. - Kullanıcı
site.com/blog?page=5gibi bir URL'e doğrudan gittiğinde, sayfa ortadan başlayacak ve hem yukarı hem de aşağı doğru kaydırma yapabilecek. - Yukarıya yeni içerik eklendiğinde kullanıcının okuduğu yer kaymayacak, yani kusursuz bir UX sunacağız.
Hazırsanız, başlayalım! 🚀
1. Proje Yapısı ve Gerekli Kütüphaneler
Bu bileşen için üç temel kütüphaneden güç alacağız:
- Vue Router: URL bazlı state yönetimi için.
- Pinia: Global state yönetimi ve post verilerimizi saklamak için.
- VueUse:
v-intersection-observerdirektifi ile DOM elemanlarının görünürlüğünü kolayca takip etmek için.
Bileşenimizin html kısmı (<template>) oldukça basit ve anlaşılır:
(blog-card componenti için aşağıdaki github linkinden kaynak kodu inceleyini.)
<template>
<div class="text-center margin-block-start-8">
<button
v-if="showLoadPreviousBtn"
@click="loadPreviousPosts"
class="padding-inline-4 padding-block-1"
>
Show More
</button>
<div v-intersection-observer="onIntersectPrevious"></div>
</div>
<div class="grid-auto-fit margin-block-start-8">
<blog-card v-for="post in store.posts" :key="post.id" :post="post" />
</div>
<div v-if="!loading" v-intersection-observer="onIntersectNext"></div>
</template>
Gördüğünüz gibi, sayfanın üstünde ve altında iki adet v-intersection-observer elemanımız var. Bunlar, kullanıcı bu noktalara geldiğinde ilgili fonksiyonları tetikleyerek veri yüklememizi sağlayacak.
2. State Yönetimi: <script setup>
Bileşenimizin beyni olan <script setup> bloğunda, tüm mantığı yöneteceğimiz reaktif değişkenlerimizi tanımlayarak işe başlayalım.
//Core
import { onMounted, shallowRef, nextTick } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
//Store
import { usePostsStore } from '@/stores/posts'
const store = usePostsStore()
//VueUse
import { vIntersectionObserver } from '@vueuse/components'
//Data
const loading = shallowRef(true) // Yüklenme durumunu tutar
const loadedPages = shallowRef({ // Hangi sayfaların yüklendiğini takip eder
min: 1,
max: 1,
})
const showLoadPreviousBtn = shallowRef(false) // "Eskileri Yükle" butonunun görünürlüğü
const isPreviousIntersectActive = shallowRef(false) // Yukarı scroll tetikleyicisinin aktif olup olmadığı
Burada en kritik değişken **loadedPages**. Bu obje, şu an ekranda hangi sayfa aralığındaki postların olduğunu (min ve max değerleriyle) takip etmemizi sağlar. shallowRef kullanmamızın sebebi, bu değişkenlerin iç içe karmaşık objeler olmaması ve performans optimizasyonu sağlamaktır.
3. Başlangıç Anı: URL’den Sayfayı Okumak
Kullanıcı sayfaya ilk girdiğinde ne olacağını onMounted ve init fonksiyonlarımızla belirliyoruz.
onMounted(async () => {
init()
})
const init = async () => {
await withLoading(async () => {
// 1. URL'deki 'page' parametresini oku
const initialPage = setPageFromQuery()
// 2. Eğer ilk sayfa değilse, "eskileri yükle" butonunu göster
checkShowLoadPreviousBtn(initialPage)
// 3. İlgili sayfanın verilerini çek
await store.fetchPosts(initialPage)
// 4. Veri gelip gelmediğini kontrol et
checkPageHasPosts()
})
}
const setPageFromQuery = () => {
const queryPage = Number(router.currentRoute.value.query.page)
const maxAllowedPage = 100 // Güvenlik ve limit için
if (queryPage >= 1 && queryPage < maxAllowedPage) {
loadedPages.min = queryPage
loadedPages.max = queryPage
return queryPage
} else {
// Geçersiz bir sayfa ise, URL'i temizleyip 1. sayfaya yönlendir
if (queryPage > maxAllowedPage || isNaN(queryPage)) {
setRouteParams(1)
}
return 1
}
}
setPageFromQuery fonksiyonu, bileşenimizin "URL Odaklı" olmasını sağlayan kilit noktadır. URL'deki page parametresini okur, geçerli bir sayı olup olmadığını kontrol eder ve başlangıç sayfamızı bu değere ayarlar. Eğer URL'de ?page=5 yazıyorsa, loadedPages objemizin hem min hem de max değeri 5 olarak başlar.
4. Aşağı Kaydırma: Yeni Yazıları Yükleme
Bu, Infinite Scroll’un klasik kısmıdır. Kullanıcı sayfanın sonuna geldiğinde onIntersectNext fonksiyonu tetiklenir.
const onIntersectNext = async (e) => {
const canLoadMore =
!loading.value && e[0].isIntersecting && store.limit * loadedPages.max < store.totalPosts
if (canLoadMore) {
await withLoading(async () => {
loadedPages.max++
setRouteParams(loadedPages.max)
await store.fetchPosts(loadedPages.max)
})
}
}
Buradaki mantık oldukça basittir: Koşullar sağlandığında loadedPages.max değerini bir artırır, URL'i günceller ve Pinia store'umuzdan yeni sayfayı getirmesini isteriz.
5. Sihirli Dokunuş: Yukarı Kaydırma ve UX’i Koruma ✨
İşte bu bileşeni özel kılan kısım! Kullanıcının eski gönderileri de yukarı kaydırarak yüklemesini sağlamak ve bunu yaparken ekranın kaymasını engellemek.
Bu işlev iki aşamalıdır:
- Manuel Tetikleme: Kullanıcı
?page=5gibi bir sayfaya geldiğinde, en başta bir "Show More" butonu görür. Bu butona tıklandığındaloadPreviousPostsfonksiyonu çalışır. - Otomatik Tetikleme: Butona bir kez tıklandıktan sonra,
isPreviousIntersectActivetrueolur ve artık sayfanın en üstündekidiv'e her gelindiğindeonIntersectPreviousotomatik olarak çalışır.
const loadPreviousPosts = async () => {
if (loadedPages.min <= 1) return
await preserveScrollPosition(async () => {
await withLoading(async () => {
loadedPages.min--
setRouteParams(loadedPages.min)
await store.fetchPosts(loadedPages.min, true)
})
})
showLoadPreviousBtn.value = false
isPreviousIntersectActive.value = true
}
Peki preserveScrollPosition ne yapıyor? Bu fonksiyon, mükemmel bir kullanıcı deneyimi için hayati öneme sahiptir.
const preserveScrollPosition = async (callback) => {
// 1. Mevcut scroll pozisyonunu ve sayfa yüksekliğini kaydet
const currentScrollTop = window.scrollY
const currentScrollHeight = document.documentElement.scrollHeight
// 2. Veri yükleme işlemini gerçekleştir (bu işlem sayfanın başına yeni DOM elemanları ekleyecek)
await callback()
await nextTick() // DOM'un güncellenmesini bekle
// 3. Yeni sayfa yüksekliğini hesapla ve aradaki farkı bul
const newScrollHeight = document.documentElement.scrollHeight
const heightDifference = newScrollHeight - currentScrollHeight
// 4. Kullanıcıyı, eklenen içeriğin yüksekliği kadar aşağı kaydır
window.scrollTo(0, currentScrollTop + heightDifference)
}
Bu fonksiyon sayesinde, sayfanın başına yeni içerikler eklendiğinde kullanıcının ekranı aniden aşağı zıplamaz. Bunun yerine, kullanıcının baktığı içerik tam olarak aynı yerde kalır. Bu, UX açısından gece ile gündüz kadar fark yaratır.
6. Pinia Store Yapısı
Tüm bu sistemin çalışması için Pinia store’umuzun da hem başa hem de sona veri eklemeyi desteklemesi gerekir. İşte basit bir store örneği:
// stores/posts.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const usePostsStore = defineStore('posts', () => {
const posts = ref([])
const totalPosts = ref(0)
const limit = 30
/**
* @param {number} page - Getirilecek sayfa numarası. API'de hangi veri bloğunu atlayacağımızı hesaplamak için kullanılır.
* @param {boolean} insertBefore - Yeni gönderilerin listenin başına mı (true) yoksa sonuna mı (false) ekleneceğini kontrol eder.
*/
const fetchPosts = async (page = 1, insertBefore = false) => {
// API'nin 'skip' parametresi için atlanacak gönderi sayısını hesaplıyoruz.
// Örneğin, page=1 ve limit=30 ise ilk 30 gönderiyi atlayıp sonrasını getirecektir.
const skip = page * limit
try {
// useFetch ile API'ye istek atıp sonucu JSON formatında alıyoruz.
const { data } = await useFetch(
`https://dummyjson.com/posts?limit=${limit}&skip=${skip}`,
).json()
// Gelen veriyi, `insertBefore` parametresine göre dizinin başına veya sonuna ekliyoruz.
// Bu, hem aşağı (yeni veri) hem de yukarı (eski veri) yönlü infinite scroll yapmamızı sağlar.
posts.value = insertBefore
? [...data.value.posts, ...posts.value] // `true` ise: Yeni veriyi mevcut listenin BAŞINA ekle.
: [...posts.value, ...data.value.posts] // `false` ise: Yeni veriyi mevcut listenin SONUNA ekle.
// Toplam gönderi sayısını API'den gelen bilgiyle güncelliyoruz.
totalPosts.value = data.value.total || 0
} catch (error) {
// Olası bir ağ hatası veya API hatası durumunda konsola bilgi yazdırıyoruz.
console.error('Gönderiler çekilirken bir hata oluştu:', error)
}
}
// Store dışından erişilebilmesi için state ve action'ları return ediyoruz.
return { posts, totalPosts, limit, fetchPosts }
})
Buradaki kilit nokta, fetchPosts aksiyonunun aldığı isPrepending parametresidir. Bu true olduğunda, gelen verileri unshift ile dizinin başına ekliyoruz. Aksi takdirde push ile sona ekliyoruz.
Sonuç
Tebrikler! Artık elinizde sadece çalışan değil, aynı zamanda kullanıcı deneyimini ve paylaşılabilirliği ön planda tutan, modern ve gelişmiş bir infinite scroll bileşeni var.
Bu yöntemle:
- Kullanıcılar tarayıcı geçmişinde gezinebilir.
- Belirli bir sayfa aralığının linkini arkadaşlarıyla paylaşabilir.
- Sayfayı yenilediklerinde kaldıkları yerden devam edebilirler.
Bu kodu alıp kendi projelerinize kolayca entegre edebilir, üzerine hata yönetimi ve daha şık yükleme animasyonları gibi ek özellikler ekleyebilirsiniz.
Umarım bu yazı faydalı olmuştur. Mutlu kodlamalar! 💻
메타데이터
- post_id
- e875cfbddeec
- slug
- query-odaklı-infinite-scroll-vue-js-e875cfbddeec
- url
- https://medium.com/@dev.cancirkin/query-odakl%C4%B1-infinite-scroll-vue-js-e875cfbddeec
- canonical_url
- https://medium.com/@dev.cancirkin/query-odakl%C4%B1-infinite-scroll-vue-js-e875cfbddeec
- author_url
- https://medium.com/@dev.cancirkin
- status
- ok
- fetched_at
- 2026-06-28 04:42:08