Skip to main content

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>

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