Advanced Data Fetching with Vue Query
Fotis Adamakis
Posted on June 18, 2023
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>
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>
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>
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>
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>
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.
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>
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.
Posted on June 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.