Documentation Index
Fetch the complete documentation index at: https://mintlify.com/TanStack/query/llms.txt
Use this file to discover all available pages before exploring further.
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>
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.