web/.agents/skills/tanstack-query-best-practices/rules/cache-invalidation.md
Query invalidation marks cached data as stale, triggering background refetches. Use targeted invalidation to refresh only affected data. Overly broad invalidation causes unnecessary network requests; too narrow invalidation leaves stale data.
// Invalidating everything after a single todo update
const mutation = useMutation({
mutationFn: updateTodo,
onSuccess: () => {
queryClient.invalidateQueries() // Invalidates ENTIRE cache
},
})
// Invalidating too broadly
const mutation = useMutation({
mutationFn: updateTodoStatus,
onSuccess: () => {
// Invalidates all todos including unrelated lists
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
// Missing invalidation of related queries
const mutation = useMutation({
mutationFn: addComment,
onSuccess: () => {
// Only invalidates comment list, misses comment count
queryClient.invalidateQueries({ queryKey: ['comments', postId] })
},
})
// Targeted invalidation with exact matching
const mutation = useMutation({
mutationFn: updateTodo,
onSuccess: (data, variables) => {
// Invalidate specific todo and related queries
queryClient.invalidateQueries({ queryKey: ['todos', variables.id] })
// Also invalidate lists that might contain this todo
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
},
})
// Use exact: true when you only want one specific query
const mutation = useMutation({
mutationFn: updateUserProfile,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['user', 'profile'],
exact: true, // Only this exact key, not ['user', 'profile', 'settings']
})
},
})
// Invalidate multiple related queries
const mutation = useMutation({
mutationFn: addComment,
onSuccess: (data, { postId }) => {
// Invalidate all comment-related queries for this post
queryClient.invalidateQueries({ queryKey: ['posts', postId, 'comments'] })
queryClient.invalidateQueries({ queryKey: ['posts', postId, 'comment-count'] })
// Optionally invalidate the post itself if it shows comment count
queryClient.invalidateQueries({ queryKey: ['posts', postId] })
},
})
// Predicate-based invalidation for complex scenarios
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
query.state.data?.userId === currentUserId,
})
// Prefix matching (default) - invalidates all matching prefixes
queryClient.invalidateQueries({ queryKey: ['todos'] })
// Matches: ['todos'], ['todos', 1], ['todos', { status: 'done' }]
// Exact matching - only the exact key
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true })
// Matches: ['todos'] only
// Predicate matching - custom logic
queryClient.invalidateQueries({
predicate: (query) => query.queryKey.includes('user-generated'),
})
// Refetch type control
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'active', // Only refetch active queries (default)
// refetchType: 'inactive' - Only inactive
// refetchType: 'all' - Both
// refetchType: 'none' - Mark stale but don't refetch
})
refetchType: 'active' (default) only refetches queries with active observerssetQueryData for optimistic updates instead of invalidation