Back to Ragflow

mut-invalidate-queries: Always Invalidate Related Queries After Mutations

web/.agents/skills/tanstack-query-best-practices/rules/mut-invalidate-queries.md

0.25.13.7 KB
Original Source

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)