Skip to main content

Quick Start

This guide will walk you through the basics of Vue Query, from installation to making your first queries and mutations.

Prerequisites

Before starting, make sure you have:
  • Vue 2.6+ or Vue 3.3+
  • Node.js 16+
  • Basic familiarity with Vue Composition API

Installation

1
1. Install the Package
2
npm
npm install @tanstack/vue-query
pnpm
pnpm add @tanstack/vue-query
yarn
yarn add @tanstack/vue-query
3
2. Set Up the Plugin
4
Initialize Vue Query in your main application file:
5
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')
6
3. Create Your First Query
7
Use useQuery in any component:
8
<script setup>
import { useQuery } from '@tanstack/vue-query'

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

const { data, isLoading, error } = useQuery({
  queryKey: ['todos'],
  queryFn: async (): Promise<Todo[]> => {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos')
    if (!response.ok) throw new Error('Failed to fetch todos')
    return response.json()
  },
})
</script>

<template>
  <div>
    <h1>Todo List</h1>
    
    <div v-if="isLoading">Loading todos...</div>
    
    <div v-else-if="error" class="error">
      Error: {{ error.message }}
    </div>
    
    <ul v-else>
      <li v-for="todo in data" :key="todo.id">
        <input type="checkbox" :checked="todo.completed" />
        {{ todo.title }}
      </li>
    </ul>
  </div>
</template>

<style scoped>
.error {
  color: red;
  padding: 1rem;
  background: #ffebee;
  border-radius: 4px;
}
</style>

Understanding Queries

Query Keys

Query keys uniquely identify queries in the cache. They can be strings or arrays:
// Simple string key
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

// Array with multiple values
const userId = ref(1)
const { data } = useQuery({
  queryKey: ['todos', userId.value],
  queryFn: () => fetchUserTodos(userId.value),
})

// Complex key with filters
const filters = ref({ status: 'active', page: 1 })
const { data } = useQuery({
  queryKey: ['todos', filters.value],
  queryFn: () => fetchTodos(filters.value),
})
When any value in the query key changes, Vue Query automatically refetches the data.

Query Functions

Query functions must return a Promise. They receive a context object with useful information:
const { data } = useQuery({
  queryKey: ['todo', todoId],
  queryFn: async ({ queryKey, signal }) => {
    const [, id] = queryKey
    
    const response = await fetch(`/api/todos/${id}`, {
      signal, // Automatic cancellation support
    })
    
    if (!response.ok) {
      throw new Error('Network response was not ok')
    }
    
    return response.json()
  },
})

Query Results

useQuery returns reactive refs with query state:
const {
  data,           // Ref<TData | undefined>
  error,          // Ref<TError | null>
  isLoading,      // Ref<boolean> - First load
  isPending,      // Ref<boolean> - Loading state
  isFetching,     // Ref<boolean> - Background refetch
  isSuccess,      // Ref<boolean> - Query succeeded
  isError,        // Ref<boolean> - Query failed
  status,         // Ref<'pending' | 'error' | 'success'>
  fetchStatus,    // Ref<'fetching' | 'paused' | 'idle'>
  refetch,        // Function to manually refetch
} = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

Working with Reactive Data

Vue Query embraces Vue’s reactivity system. Options can be refs, reactive objects, or computed values:
UserProfile.vue
<script setup>
import { ref, computed } from 'vue'
import { useQuery } from '@tanstack/vue-query'

const userId = ref(1)
const enabled = ref(true)

// Option 1: Reactive values in options object
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId.value),
  enabled, // Query only runs when enabled is true
})

// Option 2: Computed query key
const queryKey = computed(() => ['user', userId.value, 'posts'])
const { data: posts } = useQuery({
  queryKey,
  queryFn: () => fetchUserPosts(userId.value),
})

// Option 3: Options getter function
const { data: profile } = useQuery(() => ({
  queryKey: ['user', userId.value],
  queryFn: () => fetchUser(userId.value),
  enabled: enabled.value,
}))

const nextUser = () => userId.value++
const toggleEnabled = () => enabled.value = !enabled.value
</script>

<template>
  <div>
    <button @click="nextUser">Next User</button>
    <button @click="toggleEnabled">Toggle Enabled</button>
    
    <div v-if="user">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </div>
  </div>
</template>
Using reactive options is more efficient than options getter functions. Vue Query tracks dependencies automatically.

Mutations

Use useMutation to create, update, or delete data:
CreateTodo.vue
<script setup>
import { ref } from 'vue'
import { useMutation, useQueryClient } from '@tanstack/vue-query'

const queryClient = useQueryClient()
const newTodo = ref('')

const { mutate, isPending, isError, error } = useMutation({
  mutationFn: async (title: string) => {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
      method: 'POST',
      body: JSON.stringify({ title, completed: false }),
      headers: { 'Content-Type': 'application/json' },
    })
    return response.json()
  },
  onSuccess: () => {
    // Invalidate and refetch todos query
    queryClient.invalidateQueries({ queryKey: ['todos'] })
    newTodo.value = ''
  },
})

const handleSubmit = () => {
  if (newTodo.value.trim()) {
    mutate(newTodo.value)
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input 
      v-model="newTodo" 
      placeholder="Enter todo title"
      :disabled="isPending"
    />
    <button type="submit" :disabled="isPending">
      {{ isPending ? 'Adding...' : 'Add Todo' }}
    </button>
    
    <div v-if="isError" class="error">
      Error: {{ error?.message }}
    </div>
  </form>
</template>

Mutation with Optimistic Updates

Update the UI immediately while the mutation is in progress:
import { useMutation, useQueryClient } from '@tanstack/vue-query'

const queryClient = useQueryClient()

const { mutate } = useMutation({
  mutationFn: updateTodo,
  onMutate: async (updatedTodo) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['todos'] })
    
    // Snapshot previous value
    const previousTodos = queryClient.getQueryData(['todos'])
    
    // Optimistically update
    queryClient.setQueryData(['todos'], (old) => {
      return old?.map(todo => 
        todo.id === updatedTodo.id ? updatedTodo : todo
      )
    })
    
    // Return context with snapshot
    return { previousTodos }
  },
  onError: (err, variables, context) => {
    // Rollback on error
    if (context?.previousTodos) {
      queryClient.setQueryData(['todos'], context.previousTodos)
    }
  },
  onSettled: () => {
    // Always refetch after error or success
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

Dependent Queries

Enable queries only when dependencies are ready:
UserPosts.vue
<script setup>
import { computed } from 'vue'
import { useQuery } from '@tanstack/vue-query'

const { data: user } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
})

// Only fetch posts when we have a user
const { data: posts } = useQuery({
  queryKey: ['posts', user.value?.id],
  queryFn: () => fetchUserPosts(user.value!.id),
  enabled: computed(() => !!user.value?.id),
})
</script>

Parallel Queries

Execute multiple queries simultaneously:
import { useQuery } from '@tanstack/vue-query'

const { data: users } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

const { data: posts } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
})

const { data: comments } = useQuery({
  queryKey: ['comments'],
  queryFn: fetchComments,
})
Or use useQueries for dynamic parallel queries:
import { useQueries } from '@tanstack/vue-query'

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

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

// queries is an array of query results
const allLoaded = computed(() => 
  queries.value.every(q => q.isSuccess)
)

Infinite Queries

Implement infinite scroll and pagination:
InfiniteList.vue
<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'

interface PageData {
  items: any[]
  nextCursor: number | null
}

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

<template>
  <div>
    <div v-for="page in data?.pages" :key="page.nextCursor">
      <div v-for="item in page.items" :key="item.id">
        {{ item.name }}
      </div>
    </div>
    
    <button 
      @click="fetchNextPage" 
      :disabled="!hasNextPage || isFetchingNextPage"
    >
      {{ isFetchingNextPage ? 'Loading...' : 'Load More' }}
    </button>
  </div>
</template>

Query Options

Extract and reuse query configurations with queryOptions:
queries/todos.ts
import { queryOptions } from '@tanstack/vue-query'

export const todoQueryOptions = (id: number) => queryOptions({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  staleTime: 5000,
})

export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 10000,
})
Component.vue
<script setup>
import { useQuery } from '@tanstack/vue-query'
import { todoQueryOptions } from '@/queries/todos'

const todoId = ref(1)
const { data } = useQuery(todoQueryOptions(todoId.value))
</script>
queryOptions provides perfect TypeScript inference and makes query configurations reusable across queries, prefetching, and server-side code.

Error Handling

Handle errors at the query level or globally:
// Per-query error handling
const { data, error, isError } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  retry: 3,
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})

// Global error handling
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 3,
      onError: (error) => {
        console.error('Query error:', error)
        // Show toast notification, etc.
      },
    },
  },
})

Caching Behavior

Control how long data stays fresh and in cache:
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 1000 * 60 * 5,      // Data is fresh for 5 minutes
  gcTime: 1000 * 60 * 10,         // Cache for 10 minutes after last use
  refetchOnWindowFocus: true,     // Refetch when window regains focus
  refetchOnReconnect: true,       // Refetch when coming back online
})
  • staleTime: How long data is considered fresh (default: 0)
  • gcTime (formerly cacheTime): How long unused data stays in cache (default: 5 minutes)

Next Steps

TypeScript Guide

Learn about type safety and inference

DevTools

Debug queries with Vue DevTools

API Reference

Explore the complete API

Examples

View real-world examples

Build docs developers (and LLMs) love