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
npm install @tanstack/vue-query
pnpm add @tanstack/vue-query
yarn add @tanstack/vue-query
Initialize Vue Query in your main application file:
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' )
3. Create Your First Query
Use useQuery in any component:
< 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 : 1 rem ;
background : #ffebee ;
border-radius : 4 px ;
}
</ 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:
< 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:
< 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:
< 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:
< 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:
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 ,
})
< 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