Frontend DevelopmentVueJs Tutorials

Implementing Infinite Scrolling In Vue 3 Project

Implementing Infinite Scrolling In Vue 3 Project

Infinite scrolling is a sort of pagination used in many applications like social and ecommerce apps. In this article let’s learn how to build infinite scrolling in Vue 3.

 

 

In web applications typically when displaying a list of search results, we provide users a method to navigate between pages, using pagination. Pagination can implemented using different techniques. The usual technique of pagination by displaying a page numbers as links in the bottom of search page.

Another technique of pagination by using infinite scrolling, which works when scrolling to the bottom of the page and loading the results. This method implemented on many social and mobile apps.

Let’s show how to make a simple infinite scrolling component in Vue 3 project.

Assuming you have already a Vue 3 project setup, let’s create a new component in src/

src/components/Posts.vue

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import {ref} from "vue";
</script>
<template>
<h2>Infinite scroll demo</h2>
<section>
</section>
</template>
<style scoped>
h2 {
text-align: start;
}
</style>
<script setup> import {ref} from "vue"; </script> <template> <h2>Infinite scroll demo</h2> <section> </section> </template> <style scoped> h2 { text-align: start; } </style>
<script setup>

  import {ref} from "vue";


</script>

<template>
    <h2>Infinite scroll demo</h2>

    <section>
      
    </section>
</template>

<style scoped>
  h2 {
    text-align: start;
  }
</style>

In this <Posts /> component we want to display a list of posts. For our example let’s use a dummy data Api like dummyjson.com to a render a list of data.

I added the code to fetch the data and render it in the template:

Posts.vue

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import {ref} from "vue";
const items = ref([]);
const loading = ref(true);
const endpoint = 'https://dummyjson.com';
const loadData = async () => {
loading.value = true;
setTimeout(async () => {
const {posts, total} = await fetch(`${endpoint}/posts?limit=10&skip=0`).then(res => res.json());
loading.value = false;
items.value = [...items.value, ...posts];
}, 1000);
}
// Load data on page start
loadData();
</script>
<template>
<h2>Infinite scroll demo</h2>
<section>
<article v-for="post in items" :key="post.id">
<h3>{{post.title}}</h3>
<p>{{post.body}}</p>
<span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span>
</article>
<div v-if="loading" class="loader">
<i class="fa fa-spinner fa-spin"></i>
loading...
</div>
</section>
</template>
<style scoped>
h2 {
text-align: start;
}
section {
max-height: 600px;
overflow-y: auto;
}
article {
margin-bottom: 5px;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
}
.tag {
background-color: #e5e4e4;
padding: 2px 4px;
margin: 4px;
font-size: 13px;
border-radius: 8px;
}
i.fa {
margin-top: 10px;
font-size: 30px;
}
</style>
<script setup> import {ref} from "vue"; const items = ref([]); const loading = ref(true); const endpoint = 'https://dummyjson.com'; const loadData = async () => { loading.value = true; setTimeout(async () => { const {posts, total} = await fetch(`${endpoint}/posts?limit=10&skip=0`).then(res => res.json()); loading.value = false; items.value = [...items.value, ...posts]; }, 1000); } // Load data on page start loadData(); </script> <template> <h2>Infinite scroll demo</h2> <section> <article v-for="post in items" :key="post.id"> <h3>{{post.title}}</h3> <p>{{post.body}}</p> <span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span> </article> <div v-if="loading" class="loader"> <i class="fa fa-spinner fa-spin"></i> loading... </div> </section> </template> <style scoped> h2 { text-align: start; } section { max-height: 600px; overflow-y: auto; } article { margin-bottom: 5px; padding-bottom: 10px; border-bottom: 1px solid #ccc; } .tag { background-color: #e5e4e4; padding: 2px 4px; margin: 4px; font-size: 13px; border-radius: 8px; } i.fa { margin-top: 10px; font-size: 30px; } </style>
<script setup>

  import {ref} from "vue";

  const items = ref([]);
  const loading = ref(true);

  const endpoint = 'https://dummyjson.com';

  const loadData = async () => {
      loading.value = true;

      setTimeout(async () => {
        const {posts, total} = await fetch(`${endpoint}/posts?limit=10&skip=0`).then(res => res.json());
        loading.value = false;

        items.value = [...items.value, ...posts];

      }, 1000);
  }

  // Load data on page start
  loadData();

</script>

<template>
    <h2>Infinite scroll demo</h2>

    <section>
      <article v-for="post in items" :key="post.id">
        <h3>{{post.title}}</h3>
        <p>{{post.body}}</p>
        <span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span>
      </article>

      <div v-if="loading" class="loader">
        <i class="fa fa-spinner fa-spin"></i>
        loading...
      </div>
    </section>
</template>

<style scoped>
  h2 {
    text-align: start;
  }

  section {
    max-height: 600px;
    overflow-y: auto;
  }

  article {
    margin-bottom: 5px;
    padding-bottom: 10px;
    border-bottom: 1px solid #ccc;
  }

  .tag {
    background-color: #e5e4e4;
    padding: 2px 4px;
    margin: 4px;
    font-size: 13px;
    border-radius: 8px;
  }

  i.fa {
    margin-top: 10px;
    font-size: 30px;
  }
</style>

In the above code i added a function loadData() that makes an ajax request using the Javascript fetch()  API to fetch the posts from dummyposts /posts service. I declared two ref() variables as state which are items and loading.

Next in the loadData() function i set the loading and items to the returned data:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
items.value = [...items.value, ...posts];
loading.value = false;
items.value = [...items.value, ...posts]; loading.value = false;
items.value = [...items.value, ...posts];
loading.value = false;

As you see i append to the array of items every time we make a request. Then i invoked the loadData() function in the first page load:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
loadData();
loadData();
loadData();

You can also invoke loadData() inside the onMounted() lifecyle hook.

In the template we displayed the data using a v-for directive on the <article> element displaying the post title and description:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<article v-for="post in items" :key="post.id">
<h3>{{post.title}}</h3>
<p>{{post.body}}</p>
<span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span>
</article>
<article v-for="post in items" :key="post.id"> <h3>{{post.title}}</h3> <p>{{post.body}}</p> <span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span> </article>
<article v-for="post in items" :key="post.id">
        <h3>{{post.title}}</h3>
        <p>{{post.body}}</p>
        <span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span>
      </article>

In the bottom of the template i displayed the loading indicator using v-if:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<div v-if="loading" class="loader">
<i class="fa fa-spinner fa-spin"></i>
loading...
</div>
<div v-if="loading" class="loader"> <i class="fa fa-spinner fa-spin"></i> loading... </div>
<div v-if="loading" class="loader">
        <i class="fa fa-spinner fa-spin"></i>
        loading...
      </div>

 

Handling Scroll

The next step which is the subject of this tutorial is loading more data when user scroll to bottom. In this step we have to listen for the scroll event on the <section> like so:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<section v-on:scroll="loadMoreData($event)">
...
...
...
</section>
<section v-on:scroll="loadMoreData($event)"> ... ... ... </section>
<section v-on:scroll="loadMoreData($event)">

   ...
   ...
   ...
</section>

The scroll event fired when user moves the scrollbar. Now let’s define the loadMoreData() function below the loadData() function:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import {ref} from "vue";
const items = ref([]);
const loading = ref(true);
const limit = ref(10);
const skip = ref(0);
const fetchStart = ref(false);
const endpoint = 'https://dummyjson.com';
const loadData = async () => {
loading.value = true;
setTimeout(async () => {
const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json());
loading.value = false;
items.value = [...items.value, ...posts];
fetchStart.value = true;
}, 1000);
}
// Scroll handler
const loadMoreData = (ev) => {
const element = ev.srcElement;
const {scrollTop, clientHeight, scrollHeight} = element;
if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) {
fetchStart.value = false;
skip.value = items.value.length;
loadData();
}
};
// Load data on page start
loadData();
</script>
<script setup> import {ref} from "vue"; const items = ref([]); const loading = ref(true); const limit = ref(10); const skip = ref(0); const fetchStart = ref(false); const endpoint = 'https://dummyjson.com'; const loadData = async () => { loading.value = true; setTimeout(async () => { const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json()); loading.value = false; items.value = [...items.value, ...posts]; fetchStart.value = true; }, 1000); } // Scroll handler const loadMoreData = (ev) => { const element = ev.srcElement; const {scrollTop, clientHeight, scrollHeight} = element; if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) { fetchStart.value = false; skip.value = items.value.length; loadData(); } }; // Load data on page start loadData(); </script>
<script setup>

  import {ref} from "vue";

  const items = ref([]);
  const loading = ref(true);
  const limit = ref(10);
  const skip = ref(0);
  const fetchStart = ref(false);

  const endpoint = 'https://dummyjson.com';

  const loadData = async () => {
      loading.value = true;

      setTimeout(async () => {
        const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json());
        loading.value = false;

        items.value = [...items.value, ...posts];

        fetchStart.value = true;

      }, 1000);
  }

  // Scroll handler
  const loadMoreData = (ev) => {

      const element = ev.srcElement;
      const {scrollTop, clientHeight, scrollHeight} = element;

      if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) {
        fetchStart.value = false;

        skip.value = items.value.length;

        loadData();
      }

  };

  // Load data on page start
  loadData();

</script>

In this code i declared some extra ref() variables. The limit refers to the number of posts to fetch, the skip parameter refers to the start offset to start fetching the data. The limit and skip is sent along with the ajax request in /posts api, so we updated the loadData() function like so:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json());
const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json());
const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json());

The fetchStart ref is a boolean, it’s purpose is to not make too many ajax requests when user scrolls two fast to the bottom. i set this variable in loadData() function after loading the data.

The loadMoreData() function triggers when the user scrolls. I captured the scrollTop, clientHeight, and scrollHeight properties from ev.srcElement. Next we need to make the scrollbar is moved to the bottom, which is done using this check:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if (scrollTop > 0)
if (scrollTop > 0)
if (scrollTop > 0)

To ensure the user reaches the end of the page, the if condition needs to be updated:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) {}
if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) {}
if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) {}

At this point i set the fetchStart value to false and set the skip value to be the items length using items.value.length property and then calling the loadData() again to load and append more data.

Now run the application and scroll to the bottom you see more data is loading.

 

Adding debounce functionality

You can add debounce functionality when making the scroll event so that the ajax request takes some delay:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function debounce(method, delay) {
clearTimeout(method._tId);
method._tId= setTimeout(function(){
method();
}, delay);
}
function debounce(method, delay) { clearTimeout(method._tId); method._tId= setTimeout(function(){ method(); }, delay); }
function debounce(method, delay) {
    clearTimeout(method._tId);
    method._tId= setTimeout(function(){
      method();
    }, delay);
  }

Now you can wrap the loadMoreData() function body with the debounce call:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const loadMoreData = (ev) => {
debounce(() => {
const element = ev.srcElement;
const {scrollTop, clientHeight, scrollHeight} = element;
if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) {
fetchStart.value = false;
skip.value = items.value.length;
loadData();
}
}, 500);
};
const loadMoreData = (ev) => { debounce(() => { const element = ev.srcElement; const {scrollTop, clientHeight, scrollHeight} = element; if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) { fetchStart.value = false; skip.value = items.value.length; loadData(); } }, 500); };
const loadMoreData = (ev) => {
    debounce(() => {
      const element = ev.srcElement;
      const {scrollTop, clientHeight, scrollHeight} = element;

      if (scrollTop > 0 && scrollTop + clientHeight === scrollHeight && fetchStart.value) {
        fetchStart.value = false;

        skip.value = items.value.length;

        loadData();
      }
    }, 500);
  };

 

Extracting to reusable component

We can extract the infinite scroll into a reusable component:

InfiniteScroller.vue

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import {onMounted, onUnmounted} from "vue";
const props = defineProps(["loading"]);
const emits = defineEmits(["onLoadMore"]);
const handleScroll = (event) => {
emits('onLoadMore', event);
}
onMounted(() => {
window.addEventListener("scroll", handleScroll, false);
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
});
</script>
<template>
<section>
<slot />
<div v-if="loading" class="loader">
<i class="fa fa-spinner fa-spin"></i>
loading...
</div>
</section>
</template>
<script setup> import {onMounted, onUnmounted} from "vue"; const props = defineProps(["loading"]); const emits = defineEmits(["onLoadMore"]); const handleScroll = (event) => { emits('onLoadMore', event); } onMounted(() => { window.addEventListener("scroll", handleScroll, false); }); onUnmounted(() => { window.removeEventListener("scroll", handleScroll); }); </script> <template> <section> <slot /> <div v-if="loading" class="loader"> <i class="fa fa-spinner fa-spin"></i> loading... </div> </section> </template>
<script setup>
import {onMounted, onUnmounted} from "vue";
  const props = defineProps(["loading"]);
  const emits = defineEmits(["onLoadMore"]);

  const handleScroll = (event) => {
    emits('onLoadMore', event);
  }

  onMounted(() => {
    window.addEventListener("scroll", handleScroll, false);
  });

  onUnmounted(() => {
    window.removeEventListener("scroll", handleScroll);
  });
</script>

<template>
  <section>
    <slot />

    <div v-if="loading" class="loader">
      <i class="fa fa-spinner fa-spin"></i>
      loading...
    </div>
  </section>
</template>

The updated Posts.vue with <InfiniteScroller />:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import {ref} from "vue";
import InfiniteScroller from "./InfiniteScroller.vue";
const items = ref([]);
const loading = ref(true);
const limit = ref(10);
const skip = ref(0);
const fetchStart = ref(false);
const endpoint = 'https://dummyjson.com';
const loadData = async () => {
loading.value = true;
setTimeout(async () => {
const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json());
loading.value = false;
items.value = [...items.value, ...posts];
fetchStart.value = true;
}, 1000);
}
// Scroll handler
const loadMoreData = (ev) => {
const element = ev.srcElement.scrollingElement;
const {scrollTop, clientHeight, scrollHeight} = element;
if (scrollTop > 0 && scrollTop + clientHeight >= scrollHeight && fetchStart.value) {
fetchStart.value = false;
skip.value = items.value.length;
loadData();
}
};
// Load data on page start
loadData();
</script>
<template>
<InfiniteScroller
:loading="loading"
@on-load-more="loadMoreData"
>
<article v-for="post in items" :key="post.id">
<h3>{{post.title}}</h3>
<p>{{post.body}}</p>
<span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span>
</article>
</InfiniteScroller>
</template>
<style scoped>
h2 {
text-align: start;
}
article {
margin-bottom: 5px;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
}
.tag {
background-color: #e5e4e4;
padding: 2px 4px;
margin: 4px;
font-size: 13px;
border-radius: 8px;
}
i.fa {
margin-top: 10px;
font-size: 30px;
}
</style>
<script setup> import {ref} from "vue"; import InfiniteScroller from "./InfiniteScroller.vue"; const items = ref([]); const loading = ref(true); const limit = ref(10); const skip = ref(0); const fetchStart = ref(false); const endpoint = 'https://dummyjson.com'; const loadData = async () => { loading.value = true; setTimeout(async () => { const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json()); loading.value = false; items.value = [...items.value, ...posts]; fetchStart.value = true; }, 1000); } // Scroll handler const loadMoreData = (ev) => { const element = ev.srcElement.scrollingElement; const {scrollTop, clientHeight, scrollHeight} = element; if (scrollTop > 0 && scrollTop + clientHeight >= scrollHeight && fetchStart.value) { fetchStart.value = false; skip.value = items.value.length; loadData(); } }; // Load data on page start loadData(); </script> <template> <InfiniteScroller :loading="loading" @on-load-more="loadMoreData" > <article v-for="post in items" :key="post.id"> <h3>{{post.title}}</h3> <p>{{post.body}}</p> <span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span> </article> </InfiniteScroller> </template> <style scoped> h2 { text-align: start; } article { margin-bottom: 5px; padding-bottom: 10px; border-bottom: 1px solid #ccc; } .tag { background-color: #e5e4e4; padding: 2px 4px; margin: 4px; font-size: 13px; border-radius: 8px; } i.fa { margin-top: 10px; font-size: 30px; } </style>
<script setup>
import {ref} from "vue";
import InfiniteScroller from "./InfiniteScroller.vue";

const items = ref([]);
const loading = ref(true);
const limit = ref(10);
const skip = ref(0);
const fetchStart = ref(false);

const endpoint = 'https://dummyjson.com';

const loadData = async () => {
  loading.value = true;

  setTimeout(async () => {
    const {posts, total} = await fetch(`${endpoint}/posts?limit=${limit.value}&skip=${skip.value}`).then(res => res.json());
    loading.value = false;

    items.value = [...items.value, ...posts];

    fetchStart.value = true;

  }, 1000);
}

// Scroll handler
const loadMoreData = (ev) => {
    const element = ev.srcElement.scrollingElement;

    const {scrollTop, clientHeight, scrollHeight} = element;

    if (scrollTop > 0 && scrollTop + clientHeight >= scrollHeight && fetchStart.value) {
      fetchStart.value = false;

      skip.value = items.value.length;

      loadData();
    }
};

// Load data on page start
loadData();
</script>

<template>
  <InfiniteScroller
    :loading="loading"
    @on-load-more="loadMoreData"
  >

    <article v-for="post in items" :key="post.id">
      <h3>{{post.title}}</h3>
      <p>{{post.body}}</p>
      <span v-for="(tag, index) in post.tags" :key="index" class="tag">{{tag}}</span>
    </article>

  </InfiniteScroller>
</template>

<style scoped>
h2 {
  text-align: start;
}

article {
  margin-bottom: 5px;
  padding-bottom: 10px;
  border-bottom: 1px solid #ccc;
}

.tag {
  background-color: #e5e4e4;
  padding: 2px 4px;
  margin: 4px;
  font-size: 13px;
  border-radius: 8px;
}

i.fa {
  margin-top: 10px;
  font-size: 30px;
}
</style>

 

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest


0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments