Skip to main content
TanStack Query for Vue provides composables for fetching, caching, and updating asynchronous data in your Vue applications. It supports both Vue 2 (with @vue/composition-api) and Vue 3.

Installation

npm install @tanstack/vue-query
# or
pnpm add @tanstack/vue-query
# or
yarn add @tanstack/vue-query
For Vue 2, you also need to install @vue/composition-api:
npm install @vue/composition-api

Setup

Install the Vue Query plugin in your app:
import { VueQueryPlugin } from '@tanstack/vue-query'
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.use(VueQueryPlugin)
app.mount('#app')

Plugin Options

import { QueryClient } from '@tanstack/vue-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
    },
  },
})

app.use(VueQueryPlugin, {
  queryClient,
  enableDevtoolsV6Plugin: true, // Enable Vue DevTools integration
})

Core Composables

useQuery

Fetch and cache data with the useQuery composable:
<script setup>
import { useQuery } from '@tanstack/vue-query'

const { data, isLoading, error } = useQuery({
  queryKey: ['todos'],
  queryFn: async () => {
    const res = await fetch('/api/todos')
    return res.json()
  },
})
</script>

<template>
  <div>
    <div v-if="isLoading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <ul v-else>
      <li v-for="todo in data" :key="todo.id">
        {{ todo.title }}
      </li>
    </ul>
  </div>
</template>

Reactive Query Keys

Vue Query fully supports Vue’s reactivity system. Use ref or computed for dynamic queries:
<script setup>
import { ref, computed } from 'vue'
import { useQuery } from '@tanstack/vue-query'

const todoId = ref(1)

const { data } = useQuery({
  queryKey: ['todo', todoId],
  queryFn: async () => {
    const res = await fetch(`/api/todos/${todoId.value}`)
    return res.json()
  },
})

// Or use computed
const queryKey = computed(() => ['todo', todoId.value])
</script>

<template>
  <div>
    <button @click="todoId++">Next Todo</button>
    <div v-if="data">{{ data.title }}</div>
  </div>
</template>
When using refs in query options, Vue Query automatically unwraps them. You can pass todoId directly instead of todoId.value.

useMutation

Perform side effects with mutations:
<script setup>
import { useMutation, useQueryClient } from '@tanstack/vue-query'

const queryClient = useQueryClient()

const { mutate, isPending } = useMutation({
  mutationFn: async (newTodo) => {
    const res = await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(newTodo),
    })
    return res.json()
  },
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

const addTodo = () => {
  mutate({ title: 'New Todo', completed: false })
}
</script>

<template>
  <button @click="addTudo" :disabled="isPending">
    {{ isPending ? 'Adding...' : 'Add Todo' }}
  </button>
</template>

useInfiniteQuery

Implement infinite scrolling:
<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: async ({ pageParam = 0 }) => {
    const res = await fetch(`/api/posts?page=${pageParam}`)
    return res.json()
  },
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  initialPageParam: 0,
})
</script>

<template>
  <div>
    <div v-for="page in data?.pages" :key="page.id">
      <div v-for="post in page.posts" :key="post.id">
        {{ post.title }}
      </div>
    </div>
    <button
      @click="fetchNextPage()"
      :disabled="!hasNextPage || isFetchingNextPage"
    >
      {{ isFetchingNextPage ? 'Loading...' : 'Load More' }}
    </button>
  </div>
</template>

Reactivity Patterns

MaybeRef and MaybeRefDeep

Vue Query accepts reactive values throughout the options:
<script setup>
import { ref, computed } from 'vue'
import { useQuery } from '@tanstack/vue-query'

const enabled = ref(false)
const staleTime = ref(5000)
const userId = ref(1)

const { data } = useQuery({
  queryKey: ['user', userId], // ref is automatically unwrapped
  queryFn: () => fetchUser(userId.value),
  enabled, // ref
  staleTime, // ref
})
</script>

Watching Query Results

Query results are reactive refs that work with Vue’s watch:
<script setup>
import { watch } from 'vue'
import { useQuery } from '@tanstack/vue-query'

const { data, isSuccess } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

watch(data, (newData) => {
  console.log('Todos updated:', newData)
})

watch(isSuccess, (success) => {
  if (success) {
    console.log('Query succeeded!')
  }
})
</script>

Vue 2 vs Vue 3

Vue 2 with Composition API

<script>
import { defineComponent, ref } from '@vue/composition-api'
import { useQuery } from '@tanstack/vue-query'

export default defineComponent({
  setup() {
    const { data, isLoading } = useQuery({
      queryKey: ['todos'],
      queryFn: fetchTodos,
    })

    return { data, isLoading }
  },
})
</script>

Vue 3 with Script Setup

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

const { data, isLoading } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})
</script>
The API is identical between Vue 2 and Vue 3. The only difference is how you access the Composition API.

Advanced Features

useQueries

Execute multiple queries in parallel:
<script setup>
import { ref } from 'vue'
import { useQueries } from '@tanstack/vue-query'

const userIds = ref([1, 2, 3])

const results = useQueries({
  queries: userIds.value.map((id) => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
})

// results is an array of reactive query results
</script>

Query Options Factory

import { queryOptions } from '@tanstack/vue-query'

export const todoQueries = {
  all: () => queryOptions({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  }),
  detail: (id: number) => queryOptions({
    queryKey: ['todos', id],
    queryFn: () => fetchTodo(id),
  }),
}

// Usage
const { data } = useQuery(todoQueries.detail(1))

useMutationState

Track all mutations globally:
<script setup>
import { useMutationState } from '@tanstack/vue-query'

const pendingMutations = useMutationState({
  filters: { status: 'pending' },
})
</script>

<template>
  <div v-if="pendingMutations.length > 0">
    Saving {{ pendingMutations.length }} changes...
  </div>
</template>

useIsFetching

Show a global loading indicator:
<script setup>
import { useIsFetching } from '@tanstack/vue-query'

const isFetching = useIsFetching()
</script>

<template>
  <div v-if="isFetching" class="loading-bar">
    Loading...
  </div>
</template>

Shallow Option

Control ref unwrapping with the shallow option:
<script setup>
import { useQuery } from '@tanstack/vue-query'

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  shallow: true, // Prevents deep unwrapping of refs
})
</script>

TypeScript

Full TypeScript support with proper type inference:
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import type { Ref } from 'vue'

interface Todo {
  id: number
  title: string
  completed: boolean
}

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: async (): Promise<Todo[]> => {
    const res = await fetch('/api/todos')
    return res.json()
  },
})

// data is typed as Ref<Todo[] | undefined>
</script>

DevTools Integration

Vue Query integrates with Vue DevTools:
import { VueQueryPlugin } from '@tanstack/vue-query'

app.use(VueQueryPlugin, {
  enableDevtoolsV6Plugin: true, // Enables Vue DevTools integration
})

SSR Support

For Nuxt or other SSR frameworks:
// plugins/vue-query.ts
import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'

export default defineNuxtPlugin((nuxtApp) => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5000,
      },
    },
  })

  nuxtApp.vueApp.use(VueQueryPlugin, { queryClient })
})

Vue-Specific Gotchas

Ref Unwrapping: Vue Query automatically unwraps refs in query options, but query results are returned as refs. Always access them with .value in script or directly in templates.
Reactive Options: You can pass entire options objects as refs or use individual refs for specific properties. Both approaches work seamlessly with Vue’s reactivity.

Build docs developers (and LLMs) love