119 lines
3.7 KiB
Markdown
119 lines
3.7 KiB
Markdown
# 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
// 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 `onSuccess` for successful mutations
|
|
- Use `onSettled` if 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`)
|