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

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay