Vue 3 - inifinite scroll using intersection observer with composables, pinia and Typescript 🍍
Tags: vue3, javascript, vue, pinia
Published: 2023-12-18 20:27:33

Composables are typically functions or objects designed to be reusable across components with [composition Api](https://vuejs.org/guide/extras/composition-api-faq.html).
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]
```


```typescript
// 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
  }
}

```

**Pinia store**
```typescript
// 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 }
})

```


```typescript
// 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>

```

**InifiniteScroll.vue**
```typescript
<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>
```