← Back to list

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…

Hüseyin Can Çirkin · 2025-10-16 08:14 · 1 claps · 4.8 min read
#vuejs #pinia #vue-3 #composition-api #infinite-scroll
Open on Medium ↗
Wiki topics: VIS · Visual & Graphic Design 🌐 · Web Development

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=5 gibi) görünecek.
  • Kullanıcı site.com/blog?page=5 gibi 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-observer direktifi 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=5 gibi bir sayfaya geldiğinde, en başta bir "Show More" butonu görür. Bu butona tıklandığında loadPreviousPosts fonksiyonu çalışır.
  • Otomatik Tetikleme: Butona bir kez tıklandıktan sonra, isPreviousIntersectActive true olur ve artık sayfanın en üstündeki div'e her gelindiğinde onIntersectPrevious otomatik 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! 💻

Github: https://github.com/cancirkin/vue-infinite-scroll


메타데이터
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