3.7 KiB
3.7 KiB
mut-invalidate-queries: Always Invalidate Related Queries After Mutations
Priority: HIGH
Explanation
After mutations, invalidate all queries whose data might be affected. This ensures the cache stays synchronized with the server. Forgetting to invalidate related queries leads to stale UI data.
Bad Example
// No invalidation - cache remains stale
const createTodo = useMutation({
mutationFn: (newTodo) => api.createTodo(newTodo),
// Missing onSuccess handler - todo list won't show new item
})
// Partial invalidation - misses related queries
const deleteTodo = useMutation({
mutationFn: (todoId) => api.deleteTodo(todoId),
onSuccess: () => {
// Only invalidates list, not summary/counts
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
// Missing: ['todos', 'count'], ['todos', 'completed-count'], etc.
},
})
Good Example
// Comprehensive invalidation
const createTodo = useMutation({
mutationFn: (newTodo) => api.createTodo(newTodo),
onSuccess: () => {
// Invalidate all todo-related queries
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
// Targeted invalidation with all affected queries
const updateTodo = useMutation({
mutationFn: ({ id, data }) => api.updateTodo(id, data),
onSuccess: (data, { id }) => {
// Specific todo
queryClient.invalidateQueries({ queryKey: ['todos', id] })
// Lists that might contain this todo
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
// If todo status changed, invalidate filtered views
queryClient.invalidateQueries({ queryKey: ['todos', 'completed'] })
queryClient.invalidateQueries({ queryKey: ['todos', 'active'] })
},
})
// Cross-entity invalidation
const assignTodoToUser = useMutation({
mutationFn: ({ todoId, userId }) => api.assignTodo(todoId, userId),
onSuccess: (data, { todoId, userId }) => {
// Invalidate the todo
queryClient.invalidateQueries({ queryKey: ['todos', todoId] })
// Invalidate user's assigned todos
queryClient.invalidateQueries({ queryKey: ['users', userId, 'todos'] })
// Invalidate previous assignee's list if available
if (data.previousAssignee) {
queryClient.invalidateQueries({
queryKey: ['users', data.previousAssignee, 'todos'],
})
}
},
})
Pattern: Mutation with Variables Access
const mutation = useMutation({
mutationFn: updatePost,
onSuccess: (
data, // Server response
variables, // What you passed to mutate()
context // What onMutate returned
) => {
// Use variables to know which queries to invalidate
queryClient.invalidateQueries({ queryKey: ['posts', variables.id] })
queryClient.invalidateQueries({ queryKey: ['posts', 'list', variables.category] })
},
})
Pattern: Invalidate or Update Directly
// Option 1: Invalidate and refetch
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
// Option 2: Update cache directly (no network request)
onSuccess: (newTodo) => {
queryClient.setQueryData(['todos'], (old: Todo[]) => [...old, newTodo])
}
// Option 3: Hybrid - update one, invalidate others
onSuccess: (newTodo) => {
// Immediately add to list
queryClient.setQueryData(['todos', 'list'], (old: Todo[]) => [...old, newTodo])
// Invalidate counts/summaries for eventual consistency
queryClient.invalidateQueries({ queryKey: ['todos', 'count'] })
}
Context
- Place invalidation in
onSuccessfor successful mutations - Use
onSettledif you want to invalidate regardless of success/failure - Think about all UI surfaces that display related data
- For complex relationships, consider a centralized invalidation helper
- Using hierarchical query keys makes this easier (see
qk-hierarchical-organization)