Tanstack Query / Vue Query - Pagination and Infinite Scroll Example In Vue JS

Tanstack Query / Vue Query - Pagination and Infinite Scroll Example In Vue JS

Overview

Tanstack Query / Vue Query offers a powerful combination for data management in Vue.js applications. Whether you're building a small project or a large-scale application, these libraries provide the tools and flexibility needed to handle complex data requirements. By incorporating these techniques into your projects, you can enhance user experiences and optimize data fetching, leading to more efficient and enjoyable applications.

This blog post is about an example application that shows how to use Tanstack Query / vue-query in an application to implement paging and infinite query/scrolling.

Vue Query / Tanstack Query for Vue will likely: Help you remove many lines of complicated and misunderstood code from your application and replace them with just a handful of lines of Vue Query logic.

  • Make your application more maintainable and easier to build new features without worrying about wiring up new server-state data sources

  • Have a direct impact on your end-users by making your application feel faster and more responsive than ever before.

  • Potentially help you save on bandwidth and increase memory performance

Please read about VueQuery and State Management Tools Like Vuex and Pinia. I believe that by using Tanstack Query, you can simplify some of your state management code in your vue applications

I am using Ionic Framework for the application user interface, but all of the concepts in this example can be used in a vue js application without the inclusion of Ionic Framework

Getting Started

$ npm i @tanstack/vue-query
# or
$ yarn add @tanstack/vue-query

Import the VueQueryPlugin from the @tanstack/vue-query package.

This plugin adds Vue Query functionality to the application, allowing for efficient data fetching and caching.

// main.ts
import { VueQueryPlugin } from '@tanstack/vue-query';

createApp(App)
  .use(IonicVue, {
    mode: 'ios',
  })
  .use(VueQueryPlugin)
  .mount('#app');

💥 Sign Up For Our Newsletter​ - buff.ly/3lAk2jL


Application Components

The application has two primary components ExamplePaging.vue and ExampleInfinite.vue. They do basically what their name says which is to show an example of the two types of queries we discussed for the application

  • Paging - the ability to have a predefined page size and navigate forward and backward through the pages

  • Infinite - the ability to just continue to load data to the database will move a cursor through the content so you only load in the new data that you need

ExamplePaging.vue Component

Several components and functions are imported from the @ionic/vue and @tanstack/vue-query packages; the useQuery function from Vue Query is critical to what we are trying to accomplish in this section.

import { IonItem, IonLabel, IonList, IonButton, IonLoading } from "@ionic/vue";
import { Ref, ref } from "vue";
import { useQuery } from "@tanstack/vue-query";

The peopleFetcher function is defined, which takes a reactive variable page as a parameter. This function is an asynchronous function that fetches data from the Random User API based on the provided page value.

The function returns the fetched data or an empty array if the data is not available.

const peopleFetcher = async (page: Ref<number>) => {
  const response = await fetch(
    `https://randomuser.me/api/?page=${page.value}&results=20&seed=abc`
  );
  const data = await response.json();

  // fake delay
  await new Promise((resolve) => {
    setTimeout(() => resolve(true), 1000);
  });
  return data?.results || [];
};

A reactive variable page is created using the ref function from Vue; we set its value to 1 to start at the first page

The useQuery function is passed properties

  • queryKey is used as a unique identifier for the query.

  • queryFn property, which specifies the function responsible for fetching the data; it is set to use the peopleFetcher function mentioned above.

  • keepPreviousData property is set to true, which allows the previous data to be retained when fetching new data. This is useful for smooth transitions between pages and prevents the UI from flickering when navigating.

const page = ref(1);
const { isLoading, isError, data, error, isFetching, isPreviousData } =
  useQuery({
    queryKey: ["people", page],
    queryFn: () => peopleFetcher(page),
    keepPreviousData: true,
  });

The destructured values from the function call are used mostly in the template. They provide information about the current state of the query, such as whether it is loading, if there is an error, the fetched data, and so on.

The nextPage function is defined, which increments the page value by 1 if isPreviousData.value is false. This ensures that a new query is triggered only if the previous data has been fetched.

const nextPage = () => {
  if (!isPreviousData.value) {
    page.value = page.value + 1;
  }
};

The prevPage function is defined, which decrements the page value by 1 but ensures that the minimum value is 1. This allows navigating to the previous page while preventing going below the first page.

const prevPage = () => {
  page.value = Math.max(page.value - 1, 1);
};

The template for the component primarily used for rendering the data from the query. The isLoading and isFetching variables from the useQuery function are used to control specific user interface elements to create a more intuitive user experience.

<template>
  <template v-if="isLoading && !data">
    <h1><ion-loading/></h1>
  </template>
  <template v-else>
    <p>Current Page: {{ page }}</p>
    <ion-button @click="prevPage" :disabled="isFetching || page === 1">
      {{ isFetching ? "Loading..." : "Prev Page" }}
    </ion-button>

    <ion-button @click="nextPage" :disabled="isFetching">
      {{ isFetching ? "Loading..." : "Next Page" }}
    </ion-button>
    <ion-list>
      <ion-item v-for="item in data" :key="item.login.uuid">
        <ion-label>{{ item.email }}</ion-label>
      </ion-item>
    </ion-list>
  </template>
</template>

ExampleInfinite.vue Component

This component builds upon the previous one and introduces the use of the useInfiniteQuery hook from the Vue Query library. It enables infinite scrolling by fetching data in a paginated manner.

Several components and functions are imported from the @ionic/vue and @tanstack/vue-query packages; the useInfiniteQuery function from Vue Query is critical to what we are trying to accomplish in this section.

import { IonItem, IonLabel, IonList, IonButton, IonLoading } from "@ionic/vue";
import { useInfiniteQuery } from "@tanstack/vue-query";

The peopleFetcher function is defined, which takes an object as a parameter destructured with pageParam defaulting to 1. This function fetches data from the Random User API based on the provided pageParam value.

The function returns an object with two properties: pageData contains the fetched data or an empty array, and cursor represents the value of the next page to fetch. If the pageParam is 3, indicating the last page, the cursor is set to undefined to stop fetching more pages.

const peopleFetcher = async ({ pageParam = 1 }) => {
  const response = await fetch(
    `https://randomuser.me/api/?page=${pageParam}&results=10&seed=abc`
  );
  const data = await response.json();

  // fake delay
  await new Promise((resolve) => {
    setTimeout(() => resolve(true), 1000);
  });

  // sent the cursor/page value and the results
  // set max to 3 pages of data
  return {
    pageData: data?.results || [],
    cursor: pageParam === 3 ? undefined : pageParam + 1,
  };
};

The useInfiniteQuery function is passed properties:

  • queryKey property to specify a unique identifier for the query, which in this case is an array with the single element "people".

  • queryFn property is set to the peopleFetcher function

  • getNextPageParam property is a function that determines the value to use as pageParam for the next page fetch. It takes the lastPage as an argument, which contains the data and cursor value of the last fetched page.

const { data, fetchNextPage, hasNextPage, isFetching, isLoading } =
  useInfiniteQuery({
    queryKey: ["people"],
    queryFn: peopleFetcher,
    getNextPageParam: (lastPage) => {
      return lastPage.cursor;
    },
  });

The destructured values from the function call are used mostly in the template.They provide information about the current state of the query, such as whether it is loading, if there is an error, the fetched data, and it includes the fetchNextPage function which is called to get the next page of data.

The nextPage function is defined, which calls the fetchNextPage function from the hook to trigger fetching the next page of data.

const nextPage = () => {
  fetchNextPage();
};

The template for the component primarily used for rendering the data from the query.

We want to highlight how the data is returned from the query.

You can see from the template code there are nested loops. The outer loop iterates over the data?.pages array and assigns each element to the variable page. The inner loop iterates over the page.pageData array, where page is the current element from the outer loop. The item variable is used to represent each element in page.pageData.

Image description

<template>
  <h1 v-if="isLoading"><ion-loading /></h1>
  <ion-button @click="nextPage" :disabled="isFetching" v-if="hasNextPage">
    {{ isFetching ? "Loading..." : "Load More Data" }}
  </ion-button>
  <ion-list>
    <div v-for="(page, index) in data?.pages" :key="index">
      <ion-item v-for="item in page.pageData" :key="item.login.uuid">
        <ion-label>{{ item.email }}</ion-label>
      </ion-item>
    </div>
  </ion-list>
</template>

Conclusion

This is meant to be a companion to the video and the source code for individuals who like to read code!


💥 Social Media