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

650 lines
15 KiB
Markdown

# 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
```tsx
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:
```tsx
// 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:
```tsx
// 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:
```tsx
// 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:
```tsx
// 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:
```tsx
// 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:
```tsx
// 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:
```tsx
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
```
### Delayed Invalidation
Wait for mutation to settle:
```tsx
const mutation = useMutation({
mutationFn: createTodo,
onSettled: () => {
// Runs after success or error
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
```
### Conditional Invalidation
Only invalidate under certain conditions:
```tsx
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
```tsx
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'active', // Only refetch active queries (default)
});
```
### Refetch All Queries
```tsx
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all', // Refetch both active and inactive queries
});
```
### Don't Refetch
```tsx
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'none', // Only mark as stale, don't refetch
});
```
## Invalidation Patterns
### After Create
```tsx
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
```tsx
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
```tsx
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
```tsx
const markAllDone = useMutation({
mutationFn: () => fetch('/api/todos/mark-all-done', { method: 'POST' }),
onSuccess: () => {
// Invalidate all todo-related queries
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
```
## Related Query Invalidation
### Update Multiple Related Queries
```tsx
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
```tsx
// 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
```tsx
const createPost = useMutation({
mutationFn: (newPost) => createPostApi(newPost),
onSuccess: () => {
// Refetches all pages
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
```
### Invalidate Specific Pages
```tsx
queryClient.invalidateQueries({
queryKey: ['posts'],
refetchPage: (page, index) => {
// Only refetch first page
return index === 0;
},
});
```
### Selective Page Refetch
```tsx
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
```tsx
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:
```tsx
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
```tsx
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:
```tsx
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
```tsx
// 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
```tsx
// 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
```tsx
// ❌ 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:
```tsx
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
```tsx
// ❌ 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
```tsx
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
```tsx
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
```tsx
// 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**
```tsx
// ✅ Good - specific invalidation
queryClient.invalidateQueries({ queryKey: ['todos', 'list', filters] });
// ❌ Bad - too broad
queryClient.invalidateQueries({ queryKey: ['todos'] });
```
2. **Use Exact Matching When Appropriate**
```tsx
queryClient.invalidateQueries({
queryKey: ['todo', todoId],
exact: true, // Only this specific todo
});
```
3. **Invalidate in onSuccess for Success-Only**
```tsx
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
```
4. **Invalidate in onSettled for Always**
```tsx
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**
```tsx
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['todos'] }),
queryClient.invalidateQueries({ queryKey: ['stats'] }),
]);
```
7. **Use Predicate Functions for Complex Logic**
```tsx
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