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.
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