Initial commit
This commit is contained in:
649
skills/skill/references/query-invalidation.md
Normal file
649
skills/skill/references/query-invalidation.md
Normal file
@@ -0,0 +1,649 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user