# 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`)