Telerik blogs

Learn how to set up TanStack Query in a Vue app, fetch data with useQuery, leverage automatic caching and control refetching behavior.

If you have been building Vue applications for any amount of time, you’ve likely run into the same pattern over and over: call fetch or axios inside onMounted, store the result in a ref, manually track whether the request is loading, handle errors, and then figure out when and how to refetch the data when it goes stale. It works, but it doesn’t scale well, and it quickly becomes a mess of boilerplate scattered across your components.

In my previous article on Pinia state management, I mentioned TanStack Query as a powerful tool for managing asynchronous state that comes from your API. In this article, we’ll explore how to set it up in a Vue app, fetch data with useQuery, leverage automatic caching and control refetching behavior.

What Is TanStack Query?

TanStack Query (which curiously enough started as a React lib, React Query) is an async state management library that has since become framework-agnostic, with first class support for Vue. It is specifically designed to manage data that lives on your API and needs to be fetched, cached, synchronized and updated.

TanStack Query gives you out-of-the-box automatic caching, background refetching, request deduplication, loading and error states—all of this for free with the basic hands-off configuration.

It’s important to understand when to reach for TanStack Query vs. something like Pinia. Pinia is excellent for managing client state, things like UI preferences, form data, or any state that originates and lives entirely in the browser.

TanStack Query, on the other hand, shines when dealing with server state, data that comes from an API and that other users or processes could change at any time. They complement each other rather than compete.

Big disclaimer though: I highly recommend not mixing the two. I’ve yet to find a production grade application where I needed to use Pinia in conjuction with Tanstack. If you do end up needing both, make sure that you don’t overlap them. Keep your client and server data separate or it can become spaghetti code very quickly.

Setting Up TanStack Query

Setting up TanStack Query for a Vue 3 application is straightforward. First, install the package:

npm install @tanstack/vue-query
# or
yarn add @tanstack/vue-query

Then, head over to your main.js (or wherever you are mounting your app) and register the plugin.

main.js

import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'

const app = createApp(App)

app.use(VueQueryPlugin)

app.mount('#app')

That’s it! Under the hood, VueQueryPlugin creates a QueryClient for us and provides it to the entire application. If you need to customize default options, you can pass a configuration object to the plugin.

In the example below, we can set a global staleTime (the time that it takes the application to consider data that is already pulled “stale” and, thus, marks it for refetch).

app.use(VueQueryPlugin, {
  queryClientConfig: {
    defaultOptions: {
      queries: {
        staleTime: 1000 * 60 * 5, // 5 minutes
      },
    },
  },
})

With the plugin registered, we are now ready to start fetching data.

Your First Query with useQuery

The core of TanStack Query is the useQuery composable. It takes a configuration object and returns a set of reactive properties that we can use directly in our templates. Let’s build a simple component that fetches a list of users.

UserList.vue

<script setup>
import { useQuery } from '@tanstack/vue-query'

const fetchUsers = async () => {
  const response = await fetch('https://myapp.com/users')
  if (!response.ok) {
    throw new Error('Failed to fetch users')
  }
  return response.json()
}

const { data, isLoading, isError, error } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})
</script>

<template>
  <p v-if="isLoading">Loading users...</p>
  <p v-else-if="isError">Something went wrong: {{ error.message }}</p>
  <ul v-else>
    <li v-for="user in data" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>

Let’s break this down. The useQuery composable receives an object with two essential properties:

  • queryKey: An array that uniquely identifies this query. TanStack Query uses this key to cache, deduplicate and refetch the data. Think of it as a unique ID for this particular piece of server state.
  • queryFn: An async function that actually fetches the data. It must return a promise that resolves with the data or throws an error.

The composable returns several reactive properties, but the ones you’ll use most often are:

  • data: The resolved data from the query function. It’s undefined until the query resolves!
  • isLoading: A boolean that is true while the query is fetching for the first time (no cached data exists yet).
  • isFetching: A boolean that is true while the query is fetching. This includes background refetches.
  • isError: A boolean that is true if the query encountered an error.
  • error: The error object if the query failed.

Notice how clean our template is. We don’t need to manually set up loading flags or try/catch blocks. TanStack Query handles all of that for us.

I highly recommend that you read through the Query basics page from the Tanstack documentation to really understand the different states and how to manage them.

Query Keys

Query keys deserve a bit more attention. They are not just static identifiers; they can be dynamic. For example, if we wanted to fetch a single user by ID, we could write:

const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUserById(userId.value),
})

Whenever userId changes, TanStack Query will automatically recognize it as a different query and fetch the new data accordingly. If the data for that particular key was already cached, it will be returned instantly while a background refetch happens. This is extremely powerful and removes a lot of the manual watch logic you would otherwise have to write.

If you’re having a hard time wrapping your head around this one, think about what an API endpoint usually looks like:

  • myapp.com/user/1
  • myApp.com/user/2

The user portion of the queryKey closely matches our API endpoint syntax (this doesn’t have to be like this, this is merely a suggestion). More importantly, the userId reactive value will represent the ID (1/2) that we are sending to the API. We definitely want the caches for both of the calls above to be different from one another.

Caching and Stale Data

One of the biggest advantages of TanStack Query is its built-in caching. Once a query resolves, the result is stored in memory and associated with its query key. If another component (or the same component after a navigation) requests data with the same query key, the cached data is returned immediately (if it’s not stale), so no other call has to be made to the API.

This is particularly noticeable when navigating between pages. Say you have a user list on one page and a user detail on another. When your user navigates back to the list, the data will already be there. No loading spinner, no flash of empty content.

TanStack Query manages this through two important concepts:

  • staleTime: How long (in milliseconds) the data is considered “fresh.” While data is fresh, TanStack Query will never refetch it. The default is 0, meaning data is immediately considered stale after fetching.
  • gcTime (garbage collection time): How long unused cached data is kept in memory before being garbage collected. The default is 5 minutes.

You can configure these per query:

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 1000 * 60 * 10, // 10 minutes
  gcTime: 1000 * 60 * 30,    // 30 minutes
})

In this example, after the data is first fetched, it will be considered fresh for 10 minutes. During that time, any component mounting with the same ['users'] query key will get the cached data instantly with zero network requests. After 10 minutes, the data is marked stale and will be refetched the next time it is needed. If no component uses the ['users'] query for 30 minutes, the cache entry is garbage collected entirely.

To be quite honest with you, I’ve personally never bother setting or modifying the default gcTime, but it’s an important concept to keep in mind when using Tanstack.

Refetching

TanStack Query doesn’t just fetch your data once and forget about it. It comes with several mechanisms to keep your data up to date, both automatic and manual.

Automatic Refetching

By default, TanStack Query will automatically refetch stale queries in the following situations:

  • Window focus: When the user leaves your app and comes back (tabs away and returns), stale queries are refetched. This is incredibly useful and one of my favorite benefits of Tanstack that you get for free—your users always see fresh data without lifting a finger.
  • Network reconnect: If the user loses their internet connection and reconnects, stale queries are refetched automatically.

You can control these behaviors individually:

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  refetchOnWindowFocus: false, // disable window focus refetch
  refetchOnReconnect: true,    // this is the default
})

Polling with refetchInterval

For data that needs to be updated on a regular cadence, for example a notifications counter or a live feed, you can set up polling with refetchInterval.

const { data } = useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchInterval: 1000 * 30, // refetch every 30 seconds
})

This will fire a background refetch every 30 seconds regardless of user interaction and regardless of stale state. The UI updates seamlessly since the cached data is replaced when the new data arrives. Keep in mind that all data returned by useQuery is reactive.

Manual Refetching

Sometimes you want to give the user explicit control over when to refresh data. The useQuery composable returns a refetch function for exactly this purpose.

UserList.vue

<script setup>
import { useQuery } from '@tanstack/vue-query'

const { data, isLoading, refetch, isFetching } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})
</script>

<template>
  <button @click="refetch" :disabled="isFetching">
    Refresh
  </button>

  <p v-if="isLoading">Loading users...</p>
  <ul v-else>
    <li v-for="user in data" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>

Notice we’re using isFetching here instead of isLoading to control the button’s disabled state. The distinction is important: isLoading is only true when there is no cached data and a fetch is in progress (first load), while isFetching is true whenever any fetch is happening, including background refetches. This allows us to show the cached data in the list while indicating that a refresh is in progress.

Conditional Queries with Enabled

One last option worth mentioning is enabled. Sometimes you don’t want a query to fire immediately. Perhaps you need to wait for a user to select something first, or you want to set up a computed property with some conditions that need to occur first.

const { data } = useQuery({
  queryKey: ['users', userId],
  queryFn: () => fetchUserById(userId.value),
  enabled: () => !!userId.value,
})

The query will only execute when enabled evaluates to true. Once userId gets a value. Tanstack query will fire automatically once the reactive property returns true. If this property reverts to false the query no longer refetch when stale.

Wrapping Up

TanStack Query takes a lot of the pain out of data fetching in Vue applications. With minimal setup, we get automatic caching, configurable stale times, background refetching on window focus and reconnect, polling, and clean loading and error states.

We’ve only scratched the surface here. TanStack Query also supports mutations for creating and updating data, optimistic updates, infinite queries for pagination and a fantastic set of devtools for debugging your queries in the browser.

I recommend taking a deep dive into the official documentation as it is very well written and packed with examples.

Happy fetching!


About the Author

Marina Mosti

Marina Mosti is a frontend web developer with over 18 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community. In her spare time, she enjoys playing bass, drums and video games.

Related Posts

Comments

Comments are disabled in preview mode.