When you mutate data on the server, you need to update your client-side cache to reflect those changes. TanStack Query provides powerful tools for query invalidation that automatically refetch affected queries.
Basic Invalidation
The most common pattern is to invalidate queries in the mutation’s onSuccess or onSettled callback:
import { useMutation, useQueryClient } from '@tanstack/react-query'
function AddTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: async (newTodo) => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
headers: { 'Content-Type': 'application/json' },
})
return await response.json()
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<button onClick={() => mutation.mutate({ text: 'New Todo' })}>
Add Todo
</button>
)
}
invalidateQueries marks queries as stale and triggers a refetch if the query is currently being rendered.
onSuccess vs onSettled
onSuccess
Invalidate only when the mutation succeeds:
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
onSettled
Invalidate whether the mutation succeeds or fails:
const mutation = useMutation({
mutationFn: createTodo,
onSettled: () => {
// Refetch to ensure we're in sync with the server
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
Use onSettled when you want to always refetch, even after errors. This is useful with optimistic updates to ensure the cache is correct.
Invalidation Filters
Exact Matching
Invalidate only exact query key matches:
// Only invalidates ['todos']
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true,
})
// Does NOT invalidate ['todos', { status: 'done' }]
Prefix Matching
Invalidate all queries that start with a key prefix:
// Invalidates all todo-related queries:
// ['todos']
// ['todos', { status: 'done' }]
// ['todos', 1]
queryClient.invalidateQueries({
queryKey: ['todos'],
})
Predicate Filtering
Use custom logic to determine which queries to invalidate:
queryClient.invalidateQueries({
predicate: (query) => {
// Invalidate all queries with 'todos' in the key
return query.queryKey[0] === 'todos' &&
query.state.data?.length > 0
},
})
Multiple Query Invalidation
Invalidate multiple related queries:
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (data, variables) => {
// Invalidate user list
queryClient.invalidateQueries({ queryKey: ['users'] })
// Invalidate specific user
queryClient.invalidateQueries({
queryKey: ['user', variables.userId]
})
// Invalidate user's posts
queryClient.invalidateQueries({
queryKey: ['posts', { userId: variables.userId }]
})
},
})
Refetch Options
Control how invalidated queries are refetched:
// Don't refetch inactive queries
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'active', // default
})
// Refetch all matching queries, even inactive ones
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all',
})
// Only mark as stale, don't refetch
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'none',
})
Refetching all queries (including inactive ones) can be expensive. Use refetchType: 'active' unless you have a specific reason.
Awaiting Invalidation
Wait for all invalidated queries to refetch:
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: async () => {
// Wait for queries to refetch before continuing
await queryClient.invalidateQueries({ queryKey: ['todos'] })
console.log('All todos queries have been refetched')
},
})
Mutation Context Pattern
Use the context parameter in mutation callbacks:
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: (data, variables, context) => {
// Access the query client from context
context.queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
Real-World Example
Complete example with auto-refetching after mutation:
import React from 'react'
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const queryClient = useQueryClient()
const [value, setValue] = React.useState('')
const { data, isFetching } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/data')
return await response.json()
},
refetchInterval: 1000, // Auto-refetch every second
})
const addMutation = useMutation({
mutationFn: (newTodo) =>
fetch('/api/data', {
method: 'POST',
body: JSON.stringify({ text: newTodo }),
headers: { 'Content-Type': 'application/json' },
}),
onSuccess: () => {
// Invalidate to trigger immediate refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<h1>Auto Refetch</h1>
<form
onSubmit={(e) => {
e.preventDefault()
addMutation.mutate(value, {
onSuccess: () => setValue(''),
})
}}
>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Enter something"
/>
<button disabled={addMutation.isPending}>Add</button>
</form>
<ul>
{data?.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
{isFetching && (
<div style={{ color: 'green' }}>
Updating in background...
</div>
)}
</div>
)
}
Invalidation Without Refetch
Sometimes you want to mark queries as stale without immediately refetching:
// Mark as stale, will refetch on next mount/focus
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'none',
})
Query Removal
Remove queries from the cache entirely:
const mutation = useMutation({
mutationFn: deleteTodo,
onSuccess: (data, todoId) => {
// Remove specific query from cache
queryClient.removeQueries({
queryKey: ['todo', todoId],
exact: true,
})
// Invalidate list to refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
removeQueries completely removes the query from cache, while invalidateQueries marks it as stale but keeps the data.
Reset Queries
Reset queries to their initial state:
// Reset and refetch
await queryClient.resetQueries({ queryKey: ['todos'] })
// This is equivalent to:
queryClient.removeQueries({ queryKey: ['todos'] })
await queryClient.refetchQueries({ queryKey: ['todos'] })
Best Practices
Use onSettled for optimistic updates
Always invalidate in onSettled when using optimistic updates to ensure the cache is corrected even if the mutation fails.
Invalidate broad, refetch narrow
Invalidate all related queries broadly, but only active queries will refetch by default.
Leverage query key structure
Design your query keys hierarchically so you can easily invalidate related queries.
Consider manual updates
For simple mutations, updating the cache manually might be more efficient than invalidating.