DEV Community

Cover image for Advanced Data Fetching with Vue Query
Fotis Adamakis
Fotis Adamakis

Posted on • Edited on • Originally published at fadamakis.com

Advanced Data Fetching with Vue Query

One of the most challenging aspects of building a modern, large-scale application is data fetching. Features such as loading and error states, pagination, filtering, sorting, caching and many more can increase complexity and often bloat the application with a lot of boilerplate code.

That’s where the Vue Query library comes in. It handles and simplifies data fetching with a declarative syntax and treats all of those repetitive tasks for us, behind the scenes.

Understanding Vue Query

Vue query is not a replacement for Axios or fetch. It is an abstraction layer on top of them.

The challenges when managing server state is different and more complicated than managing client state. We need to solve:

  • Caching… (possibly the hardest thing to do in programming)

  • Deduping multiple requests for the same data into a single request

  • Updating out-of-date data in the background

  • Knowing when data is out of date

  • Reflecting updates to data as quickly as possible

  • Performance optimizations like pagination and lazy loading

  • Managing memory and garbage collection of server state

  • Memoizing query results with structural sharing

Vue Query is awesome because it hides all of this complexity from us. It is configured by default based on best practices but also provides a way to change this configuration if needed.

Basic Example Usage

Let me showcase the library by building the following simple application with you.

On a page level, we need to fetch all the products, display them in a table and have some simple additional logic to select one of them.

    <!-- Page component without Vue-Query -->
    <script setup>
    import { ref } from "vue";
    import BoringTable from "@/components/BoringTable.vue";
    import ProductModal from "@/components/ProductModal.vue";

    const data = ref();
    const loading = ref(false);

    async function fetchData() {
      loading.value = true;
      const response = await fetch(
        `https://dummyjson.com/products?limit=10`
      ).then((res) => res.json());
      data.value = response.products;
      loading.value = false;
    }

    fetchData();

    const selectedProduct = ref();

    function onSelect(item) {
      selectedProduct.value = item;
    }
    </script>

    <template>
      <div class="container">
        <ProductModal
          v-if="selectedProduct"
          :product-id="selectedProduct.id"
          @close="selectedProduct = null"
        />
        <BoringTable :items="data" v-if="!loading" @select="onSelect" />
      </div>
    </template>
Enter fullscreen mode Exit fullscreen mode

In case of a product selection, we will show a modal and fetch the additional product information while a loading state is shown.

    <!-- Modal component without Vue-Query -->
    <script setup>
    import { ref } from "vue";
    import GridLoader from 'vue-spinner/src/GridLoader.vue'

    const props = defineProps({
      productId: {
        type: String,
        required: true,
      },
    });

    const emit = defineEmits(["close"]);

    const product = ref();
    const loading = ref(false);

    async function fetchProduct() {
      loading.value = true;
      const response = await fetch(
        `https://dummyjson.com/products/${props.productId}`
      ).then((res) => res.json());
      product.value = response;
      loading.value = false;
    }

    fetchProduct();
    </script>

    <template>
      <div class="modal">
        <div class="modal__content" v-if="loading">
          <GridLoader />
        </div>
        <div class="modal__content" v-else-if="product">
          // modal content omitted
        </div>
      </div>
      <div class="modal-overlay" @click="emit('close')"></div>
    </template>
Enter fullscreen mode Exit fullscreen mode

Adding Vue Query

The library comes preconfigured with aggressive but sane defaults. This means that for basic usage we don’t have to do much.

    <script setup>
    import { useQuery } from "vue-query";

    function fetchData() {
      // Make api call here
    }

    const { isLoading, data } = useQuery(
      "uniqueKey",
      fetchData
    );
    </script>

    <template>
      {{ isLoading }}
      {{ data }}
    </template>
Enter fullscreen mode Exit fullscreen mode

In the example above:

  • uniqueKey is a unique identifier used for caching

  • fetchData is a function that returns a promise with the API call

  • isLoading indicates if the API call has been fulfilled yet

  • data is the response to the API call

Let's incorporate this into our example:

    <!-- Page component with Vue-Query -->
    <script setup>
    import { ref } from "vue";
    import { useQuery } from "vue-query";

    import BoringTable from "@/components/BoringTable.vue";
    import OptimisedProductModal from "@/components/OptimisedProductModal.vue";

    async function fetchData() {
      return await fetch(`https://dummyjson.com/products?limit=10`).then((res) => res.json());
    }

    const { isLoading, data } = useQuery(
      "products",
      fetchData
    );

    const selectedProduct = ref();

    function onSelect(item) {
      selectedProduct.value = item;
    }
    </script>

    <template>
      <div class="container">
        <OptimisedProductModal
          v-if="selectedProduct"
          :product-id="selectedProduct.id"
          @close="selectedProduct = null"
        />
        <BoringTable :items="data.products" v-if="!isLoading" @select="onSelect" />
      </div>
    </template>
Enter fullscreen mode Exit fullscreen mode

The fetch function is now simplified since the loading state is handled by the library.

The same applies to the modal component:

    <!-- Modal component with Vue-Query -->
    <script setup>
    import GridLoader from 'vue-spinner/src/GridLoader.vue'
    import { useQuery } from "vue-query";

    const props = defineProps({
      productId: {
        type: String,
        required: true,
      },
    });

    const emit = defineEmits(["close"]);

    async function fetchProduct() {
      return await fetch(
        `https://dummyjson.com/products/${props.productId}`
      ).then((res) => res.json());
    }

    const { isLoading, data: product } = useQuery(
      ["product", props.productId],
      fetchProduct
    );

    </script>

    <template>
      <div class="modal">
        <div class="modal__content" v-if="isLoading">
          <GridLoader />
        </div>
        <div class="modal__content" v-else-if="product">
          // modal content omitted
        </div>
      </div>
      <div class="modal-overlay" @click="emit('close')"></div>
    </template>
Enter fullscreen mode Exit fullscreen mode

Two things to notice above:

  • useQuery returns the response with the name data and in order to rename it we can use es6 destructure like this const { data: product } = useQuery(...) This is also useful when multiple queries are performed on the same page.

  • A simple string for identifier will not work since the same function will be used for all the products. We need to provide the product id as well ["product", props.productId]

We didn't do much but we got a lot out of the box. First of all, the performance improvement from caching when re-visiting a product is evident even without network throttling.

Try this out yourself

By default, cached data are considered stale. They are re-fetched automatically in the background when:

  • New instances of the query mount

  • The window is refocused

  • The network is reconnected.

  • The query is optionally configured with a re-fetch interval.

Additionally, queries that fail are silently retried 3 times, with exponential backoff delay before capturing and displaying an error to the UI.

Adding Error Handling

So far our code has good faith that the API call will not fail. But in a real-world application, this is not always the case. Error handling should be implemented within a try-catch block and some additional variables would be needed to handle the error state. Thankfully vue-query provides a more intuitive way of this by providing an isError and error variables.

    <script setup>
    import { useQuery } from "vue-query";

    function fetchData() {
      // Make api call here
    }

    const { data, isError, error } = useQuery(
      "uniqueKey",
      fetchData
    );
    </script>

    <template>
      {{ data }}
      <template v-if="isError">
        An error has occurred: {{ error }}
      </template>
    </template>
Enter fullscreen mode Exit fullscreen mode

Conclusion

In conclusion, Vue Query simplifies data fetching by replacing complex boilerplate code with a few lines of intuitive Vue Query logic. This improves maintainability and allows for seamlessly wiring up new server data sources.

The direct impact is faster and more responsive applications, potentially saving on bandwidth and increasing memory performance. Additionally, some advanced features we didn't mention such as prefetching, paginated queries, dependent queries offer further flexibility and should cover all of your needs.

If you are working on a medium to large-scale application you should definitely consider adding Vue Query to your codebase.

Top comments (0)