Back to Ragflow

cache-invalidation: Use Targeted Invalidation Over Broad Patterns

web/.agents/skills/tanstack-query-best-practices/rules/cache-invalidation.md

0.25.13.6 KB
Original Source

cache-invalidation: Use Targeted Invalidation Over Broad Patterns

Priority: CRITICAL

Explanation

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.

Bad Example

tsx
// 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] })
  },
})

Good Example

tsx
// 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,
})

Invalidation Patterns

tsx
// 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
})

Context

  • Invalidation only marks queries as stale; refetch happens when query is used
  • refetchType: 'active' (default) only refetches queries with active observers
  • Use hierarchical query keys to enable precise invalidation
  • Consider setQueryData for optimistic updates instead of invalidation
  • Always test invalidation patterns to ensure all affected queries are refreshed