Files
gh-tenequm-claude-plugins-t…/skills/skill/references/query-invalidation.md
2025-11-30 09:01:30 +08:00

15 KiB

Query Invalidation

Query invalidation is the process of marking queries as stale and potentially refetching them. This is essential for keeping your cache in sync with server state after mutations.

Basic Invalidation

import { useMutation, useQueryClient } from '@tanstack/react-query';

function CreateTodo() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then(res => res.json());
    },
    onSuccess: () => {
      // Mark todos queries as stale and refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <button onClick={() => mutation.mutate({ title: 'New Todo' })}>
      Create Todo
    </button>
  );
}

Invalidation Methods

invalidateQueries

Marks queries as stale and triggers refetch of active queries:

// Invalidate all queries
queryClient.invalidateQueries();

// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['todos'] });

// Invalidate query with exact match
queryClient.invalidateQueries({ queryKey: ['todo', todoId], exact: true });

// Invalidate and wait for refetch
await queryClient.invalidateQueries({ queryKey: ['todos'] });

refetchQueries

Directly refetch queries without marking as stale first:

// Refetch all queries
queryClient.refetchQueries();

// Refetch specific queries
queryClient.refetchQueries({ queryKey: ['todos'] });

// Refetch only active queries
queryClient.refetchQueries({ queryKey: ['todos'], type: 'active' });

// Refetch only inactive queries
queryClient.refetchQueries({ queryKey: ['todos'], type: 'inactive' });

resetQueries

Reset queries to their initial state:

// Reset and refetch
queryClient.resetQueries({ queryKey: ['todos'] });

// Reset specific query
queryClient.resetQueries({ queryKey: ['todo', todoId] });

Query Key Matching

Prefix Matching

By default, invalidateQueries uses prefix matching:

// This query
useQuery({ queryKey: ['todos', 'list', { page: 1 }], queryFn: fetchTodos });

// Is invalidated by any of these:
queryClient.invalidateQueries({ queryKey: ['todos'] });
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });
queryClient.invalidateQueries({ queryKey: ['todos', 'list', { page: 1 }] });

// But NOT by these:
queryClient.invalidateQueries({ queryKey: ['todos', 'detail'] });
queryClient.invalidateQueries({ queryKey: ['users'] });

Exact Matching

Use exact: true for precise matching:

// Only invalidate this exact query key
queryClient.invalidateQueries({
  queryKey: ['todos', 'list', { page: 1 }],
  exact: true,
});

// This would invalidate:
useQuery({ queryKey: ['todos', 'list', { page: 1 }], ... });

// But NOT these:
useQuery({ queryKey: ['todos', 'list', { page: 2 }], ... });
useQuery({ queryKey: ['todos', 'list'], ... });
useQuery({ queryKey: ['todos'], ... });

Predicate Functions

Use custom matching logic:

// Invalidate all todos queries except detail queries
queryClient.invalidateQueries({
  predicate: (query) => {
    return query.queryKey[0] === 'todos' && query.queryKey[1] !== 'detail';
  },
});

// Invalidate stale queries only
queryClient.invalidateQueries({
  predicate: (query) => {
    return query.state.isInvalidated;
  },
});

// Invalidate based on query data
queryClient.invalidateQueries({
  predicate: (query) => {
    const data = query.state.data as Todo[] | undefined;
    return data?.some((todo) => todo.userId === targetUserId) ?? false;
  },
});

Invalidation Timing

Immediate Invalidation

Invalidate and refetch immediately:

const mutation = useMutation({
  mutationFn: createTodo,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Delayed Invalidation

Wait for mutation to settle:

const mutation = useMutation({
  mutationFn: createTodo,
  onSettled: () => {
    // Runs after success or error
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Conditional Invalidation

Only invalidate under certain conditions:

const mutation = useMutation({
  mutationFn: updateTodo,
  onSuccess: (data, variables) => {
    if (data.isPublished) {
      // Only invalidate if todo was published
      queryClient.invalidateQueries({ queryKey: ['todos', 'published'] });
    }
  },
});

Refetch Strategies

Refetch Active Queries Only

queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'active', // Only refetch active queries (default)
});

Refetch All Queries

queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'all', // Refetch both active and inactive queries
});

Don't Refetch

queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'none', // Only mark as stale, don't refetch
});

Invalidation Patterns

After Create

const createTodo = useMutation({
  mutationFn: (newTodo) => fetch('/api/todos', { method: 'POST', ... }),
  onSuccess: () => {
    // Invalidate list queries to show new item
    queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });
  },
});

After Update

const updateTodo = useMutation({
  mutationFn: ({ id, updates }) => fetch(`/api/todos/${id}`, { method: 'PATCH', ... }),
  onSuccess: (data, { id }) => {
    // Invalidate specific item
    queryClient.invalidateQueries({ queryKey: ['todo', id] });
    // Invalidate list in case item moved categories, etc.
    queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });
  },
});

After Delete

const deleteTodo = useMutation({
  mutationFn: (id) => fetch(`/api/todos/${id}`, { method: 'DELETE' }),
  onSuccess: (_, id) => {
    // Remove specific item from cache
    queryClient.removeQueries({ queryKey: ['todo', id] });
    // Invalidate lists
    queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });
  },
});

Bulk Operations

const markAllDone = useMutation({
  mutationFn: () => fetch('/api/todos/mark-all-done', { method: 'POST' }),
  onSuccess: () => {
    // Invalidate all todo-related queries
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});
const updateUser = useMutation({
  mutationFn: ({ userId, updates }) => updateUserApi(userId, updates),
  onSuccess: (data, { userId }) => {
    // Invalidate user detail
    queryClient.invalidateQueries({ queryKey: ['user', userId] });
    // Invalidate user list
    queryClient.invalidateQueries({ queryKey: ['users'] });
    // Invalidate user's posts
    queryClient.invalidateQueries({ queryKey: ['posts', 'user', userId] });
    // Invalidate user's comments
    queryClient.invalidateQueries({ queryKey: ['comments', 'user', userId] });
  },
});

Hierarchical Invalidation

// Query key structure:
// ['todos'] - all todos
// ['todos', 'list'] - todo lists
// ['todos', 'list', filters] - filtered lists
// ['todos', 'detail'] - todo details
// ['todos', 'detail', id] - specific todo

const updateTodo = useMutation({
  mutationFn: updateTodoApi,
  onSuccess: (data, { id }) => {
    // Invalidate specific todo detail
    queryClient.invalidateQueries({ queryKey: ['todos', 'detail', id] });
    // Invalidate all list queries (they might show this todo)
    queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });
  },
});

Invalidation with Infinite Queries

Invalidate All Pages

const createPost = useMutation({
  mutationFn: (newPost) => createPostApi(newPost),
  onSuccess: () => {
    // Refetches all pages
    queryClient.invalidateQueries({ queryKey: ['posts'] });
  },
});

Invalidate Specific Pages

queryClient.invalidateQueries({
  queryKey: ['posts'],
  refetchPage: (page, index) => {
    // Only refetch first page
    return index === 0;
  },
});

Selective Page Refetch

const updatePost = useMutation({
  mutationFn: ({ id, updates }) => updatePostApi(id, updates),
  onSuccess: (data, { id }) => {
    queryClient.invalidateQueries({
      queryKey: ['posts'],
      refetchPage: (page, index) => {
        // Only refetch pages containing this post
        return page.posts.some((post) => post.id === id);
      },
    });
  },
});

Advanced Invalidation

Cascading Invalidation

const deleteProject = useMutation({
  mutationFn: (projectId) => deleteProjectApi(projectId),
  onSuccess: async (_, projectId) => {
    // Step 1: Remove project from cache
    queryClient.removeQueries({ queryKey: ['project', projectId] });

    // Step 2: Invalidate project list
    await queryClient.invalidateQueries({ queryKey: ['projects'] });

    // Step 3: Invalidate related resources
    await queryClient.invalidateQueries({ queryKey: ['tasks', 'project', projectId] });
    await queryClient.invalidateQueries({ queryKey: ['members', 'project', projectId] });

    // Step 4: Invalidate summary/stats
    await queryClient.invalidateQueries({ queryKey: ['stats'] });
  },
});

Debounced Invalidation

For frequent updates, debounce invalidation:

import { useDebouncedCallback } from 'use-debounce';

function SearchableList() {
  const queryClient = useQueryClient();

  const debouncedInvalidate = useDebouncedCallback(() => {
    queryClient.invalidateQueries({ queryKey: ['search-results'] });
  }, 500);

  const updateFilters = (newFilters) => {
    setFilters(newFilters);
    debouncedInvalidate();
  };

  return <FilterPanel onChange={updateFilters} />;
}

Throttled Invalidation

import { throttle } from 'lodash';

const throttledInvalidate = throttle(() => {
  queryClient.invalidateQueries({ queryKey: ['live-data'] });
}, 1000);

// In a websocket listener
socket.on('update', () => {
  throttledInvalidate();
});

Query Filters

Use query filters for more complex matching:

import { QueryFilters } from '@tanstack/react-query';

const filters: QueryFilters = {
  queryKey: ['todos'],
  type: 'active',        // 'active' | 'inactive' | 'all'
  stale: true,           // Only stale queries
  exact: false,          // Prefix matching
  predicate: (query) => {
    // Custom logic
    return query.state.dataUpdatedAt > Date.now() - 60000;
  },
};

queryClient.invalidateQueries(filters);

Filter by State

// Only invalidate stale queries
queryClient.invalidateQueries({
  queryKey: ['todos'],
  stale: true,
});

// Only invalidate fetching queries
queryClient.invalidateQueries({
  predicate: (query) => query.state.fetchStatus === 'fetching',
});

Filter by Type

// Only active queries (currently mounted)
queryClient.invalidateQueries({
  queryKey: ['todos'],
  type: 'active',
});

// Only inactive queries (not mounted)
queryClient.invalidateQueries({
  queryKey: ['todos'],
  type: 'inactive',
});

// All queries
queryClient.invalidateQueries({
  queryKey: ['todos'],
  type: 'all',
});

Performance Considerations

Batch Invalidations

// ❌ Multiple separate invalidations
queryClient.invalidateQueries({ queryKey: ['todos'] });
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['projects'] });

// ✅ Batch with predicate
queryClient.invalidateQueries({
  predicate: (query) => {
    const key = query.queryKey[0];
    return key === 'todos' || key === 'users' || key === 'projects';
  },
});

Smart Invalidation

Only invalidate what's needed:

const updateTodo = useMutation({
  mutationFn: updateTodoApi,
  onSuccess: (data, { id, updates }) => {
    // If only title changed, no need to invalidate lists
    if (Object.keys(updates).length === 1 && 'title' in updates) {
      queryClient.invalidateQueries({ queryKey: ['todo', id], exact: true });
    } else {
      // Status/category changed, invalidate lists too
      queryClient.invalidateQueries({ queryKey: ['todo', id] });
      queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });
    }
  },
});

Prevent Over-Invalidation

// ❌ Too broad - invalidates everything
queryClient.invalidateQueries();

// ❌ Still too broad - invalidates all todo queries
queryClient.invalidateQueries({ queryKey: ['todos'] });

// ✅ Specific - only invalidates affected queries
queryClient.invalidateQueries({ queryKey: ['todos', 'list', filters] });
queryClient.invalidateQueries({ queryKey: ['todo', todoId] });

Alternatives to Invalidation

Sometimes you don't need invalidation:

Direct Cache Update

const toggleTodo = useMutation({
  mutationFn: (todoId) => toggleTodoApi(todoId),
  onSuccess: (data, todoId) => {
    // Directly update cache instead of invalidating
    queryClient.setQueryData(['todo', todoId], data);
    queryClient.setQueryData(['todos'], (old) =>
      old?.map((todo) => (todo.id === todoId ? data : todo))
    );
  },
});

Optimistic Updates

const updateTodo = useMutation({
  mutationFn: updateTodoApi,
  onMutate: async ({ id, updates }) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    const previous = queryClient.getQueryData(['todos']);

    // Update cache optimistically
    queryClient.setQueryData(['todos'], (old) =>
      old?.map((todo) => (todo.id === id ? { ...todo, ...updates } : todo))
    );

    return { previous };
  },
  onError: (err, vars, context) => {
    queryClient.setQueryData(['todos'], context.previous);
  },
  // No need for invalidation if optimistic update is accurate
});

Polling

// Instead of manual invalidation, use automatic refetching
useQuery({
  queryKey: ['live-data'],
  queryFn: fetchLiveData,
  refetchInterval: 5000, // Auto-refetch every 5 seconds
});

Best Practices

  1. Be Specific with Query Keys

    // ✅ Good - specific invalidation
    queryClient.invalidateQueries({ queryKey: ['todos', 'list', filters] });
    
    // ❌ Bad - too broad
    queryClient.invalidateQueries({ queryKey: ['todos'] });
    
  2. Use Exact Matching When Appropriate

    queryClient.invalidateQueries({
      queryKey: ['todo', todoId],
      exact: true, // Only this specific todo
    });
    
  3. Invalidate in onSuccess for Success-Only

    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    }
    
  4. Invalidate in onSettled for Always

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    }
    
  5. Consider Alternatives

    • Direct cache updates for simple changes
    • Optimistic updates for better UX
    • Polling for real-time data
  6. Batch Related Invalidations

    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['todos'] }),
      queryClient.invalidateQueries({ queryKey: ['stats'] }),
    ]);
    
  7. Use Predicate Functions for Complex Logic

    queryClient.invalidateQueries({
      predicate: (query) => {
        // Custom matching logic
        return shouldInvalidate(query);
      },
    });
    
  8. Monitor Invalidation Performance

    • Use React Query DevTools
    • Check for unnecessary refetches
    • Optimize query key structure