DEV Community

hichem ben chaabene
hichem ben chaabene

Posted on

Vue 3 - inifinite scroll using intersection observer with composables, pinia and Typescript 🍍

Composables are typically functions or objects designed to be reusable across components with composition Api.
They are a way to encapsulate and distribute pieces of logic.

In this article, i'm going to share an implementation of a real life application use case of composables such as infinite scroll where typically we want to load more items when the user scroll down in the view.

Also please ignore the css, html and services as these you can do on your own and my specific example isn't necessary something i want to cover here.

The final code:

- useIsInView.ts [comopsable]
- infiniteScroll.vue [component]
- movies.vue [component]
- useMovies.ts [pinia store]
Enter fullscreen mode Exit fullscreen mode
// useIsInView.ts
import { ref, onMounted } from 'vue'
import type { Ref } from 'vue'

interface IntersectionObserverOptions {
  root: Element | Document | null
  rootMargin: string
  threshold: number | number[]
  trackVisibility: boolean
  delay: number
}

const defaultOptions: IntersectionObserverOptions = {
  root: null,
  rootMargin: '0px',
  threshold: 0,
  trackVisibility: false,
  delay: 0
}

export function useIsInView(
  elementRef: Ref<HTMLElement | Element>,
  options: Partial<IntersectionObserverOptions> = defaultOptions
) {
  const isInView = ref<boolean>(false)

  function handleIntersection(entries: IntersectionObserverEntry[]) {
    const intersecting = entries[0].isIntersecting
    isInView.value = intersecting
  }

  onMounted(() => {
    const observer = new IntersectionObserver(handleIntersection, {
      ...defaultOptions,
      ...options
    })
    observer.observe(elementRef.value)
  })
  return {
    isInView
  }
}

Enter fullscreen mode Exit fullscreen mode

Pinia store

// useMovies.ts
import { ref, MaybeRef } from 'vue'
import { defineStore } from 'pinia'
import { fetchMovie } from '@/services/tmdb'
import type { MoviesList } from '@/services/tmbd'

export const useMoviesStore = defineStore('movies', () => {
  const page = ref<number>(1)
  const movies = ref<MabyeRef<MoviesList>>(null)
  const isLoading = ref<boolean>(false)
  const error = ref<any>(null)

  function nextPage() {
    page.value++
    fetchMovies()
  }

  function setMovies(data: unknown) {
    movies.value = movies.value.concat(data.results)
  }

  function setLoading(loading: boolean) {
    isLoading.value = loading
  }

  async function fetchMovies(): Promise<void> {
    try {
      setLoading(true)
      const data = await fetchMovie('movie', { page: page.value })
      setMovies(data)
    } catch (error) {
      // handle your error
    } finally {
      setLoading(false)
    }
  }
  return { nextPage, movies, fetchMovies, isLoading, error }
})

Enter fullscreen mode Exit fullscreen mode
// movies component
<script setup lang="ts">
import { onMounted, toRefs } from 'vue'
import { useMoviesStore } from '@/stores/movies'
import Movie from '@/components/Movie.vue'
import infiniteScroll from '@/components/InfiniteScroll.vue'

const { movies } = toRefs(useMoviesStore())

onMounted(async () => {
  await useMoviesStore().fetchMovies()
})

const fetchNexPage = () => {
  if (movies.value && movies.value.length > 0) {
    useMoviesStore().nextPage()
  }
}
</script>

<template>
  <div>
    <div v-for="movie in movies" :key="movie.id">
      <movie :movie="movie" />
    </div>
    <infinite-scroll @in-view="fetchNexPage"></infinite-scroll>
  </div>
</template>

Enter fullscreen mode Exit fullscreen mode

InifiniteScroll.vue

<script setup lang="ts">
import { MaybeRef, ref, watch } from 'vue'
import { useIsInView } from '@/composables/useIsInView'

const infiniteScroll = ref<MaybeRef<Element>>(null)
const emit = defineEmits(['in-view'])
const { isInView } = useIsInView(infiniteScroll)

watch(isInView, (newValue) => {
  if (newValue) {
    emit('in-view')
  }
})
</script>

<template>
  <div ref="infiniteScroll"></div>
</template>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)