Initial commit

This commit is contained in:
allagroup
2026-04-17 07:21:17 +00:00
commit fe38508201
470 changed files with 74105 additions and 0 deletions

View File

@@ -0,0 +1,472 @@
---
name: tanstack-query
description: TanStack Query v5 data fetching patterns including useSuspenseQuery, useQuery, mutations, cache management, and API service integration. Use when fetching data, managing server state, or working with TanStack Query hooks.
---
# TanStack Query Patterns
## Purpose
Modern data fetching with TanStack Query v5 (latest: 5.90.5, November 2025), emphasizing Suspense-based queries, cache-first strategies, and centralized API services.
**Note**: v5 (released October 2023) has breaking changes from v4:
- `isLoading``isPending` for status
- `cacheTime``gcTime` (garbage collection time)
- React 18.0+ required
- Callbacks removed from useQuery (onError, onSuccess, onSettled)
- `keepPreviousData` replaced with `placeholderData` function
## When to Use This Skill
- Fetching data with TanStack Query
- Using useSuspenseQuery or useQuery
- Managing mutations
- Cache invalidation and updates
- API service patterns
---
## Quick Start
### Primary Pattern: useSuspenseQuery
For **all new components**, use `useSuspenseQuery`:
```typescript
import { useSuspenseQuery } from '@tanstack/react-query';
import { postsApi } from '~/features/posts/api/postsApi';
function PostList() {
const { data: posts } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// Wrap with Suspense
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>
```
**Benefits:**
- No `isLoading` checks needed
- Integrates with Suspense boundaries
- Cleaner component code
- Consistent loading UX
---
## useSuspenseQuery Patterns
### Basic Usage
```typescript
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => userApi.get(userId),
});
// data is never undefined - guaranteed by Suspense
return <div>{data.name}</div>;
```
### With Parameters
```typescript
function UserPosts({ userId }: { userId: string }) {
const { data: posts } = useSuspenseQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => postsApi.getByUser(userId),
});
return <div>{posts.length} posts</div>;
}
```
### Dependent Queries
```typescript
function PostDetails({ postId }: { postId: string }) {
// First query
const { data: post } = useSuspenseQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
// Second query depends on first
const { data: author } = useSuspenseQuery({
queryKey: ['users', post.authorId],
queryFn: () => userApi.get(post.authorId),
});
return <div>{author.name} wrote {post.title}</div>;
}
```
---
## useQuery (Legacy Pattern)
Use `useQuery` only when you need loading/error states in the component:
```typescript
import { useQuery } from '@tanstack/react-query';
function Component() {
const { data, isPending, error } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
if (isPending) return <Spinner />;
if (error) return <Error error={error} />;
return <div>{data.map(...)}</div>;
}
```
**When to use `useQuery` vs `useSuspenseQuery`:**
- Use `useSuspenseQuery` by default (preferred)
- Use `useQuery` only when you need component-level loading states
- Most cases should use `useSuspenseQuery` + Suspense boundaries
---
## Mutations
### Basic Mutation
```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePostButton() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: postsApi.create,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleCreate = () => {
mutation.mutate({
title: 'New Post',
content: 'Content here',
});
};
return (
<button onClick={handleCreate} disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
);
}
```
### Optimistic Updates
```typescript
const mutation = useMutation({
mutationFn: postsApi.update,
onMutate: async (updatedPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });
// Snapshot previous value
const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);
// Optimistically update
queryClient.setQueryData(['posts', updatedPost.id], updatedPost);
// Return context with snapshot
return { previousPost };
},
onError: (err, updatedPost, context) => {
// Rollback on error
queryClient.setQueryData(['posts', updatedPost.id], context.previousPost);
},
onSettled: (data, error, variables) => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });
}
});
```
---
## Cache Management
### Invalidation
```typescript
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
// Invalidate all posts queries
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Invalidate specific post
queryClient.invalidateQueries({ queryKey: ['posts', postId] });
// Invalidate all queries
queryClient.invalidateQueries();
```
### Manual Updates
```typescript
// Update cache directly
queryClient.setQueryData(['posts', postId], newPost);
// Update with function
queryClient.setQueryData(['posts'], (oldPosts) => [...oldPosts, newPost]);
```
### Prefetching
```typescript
// Prefetch data
await queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
// In a component
const prefetchPost = (postId: string) => {
queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
};
<Link
to={`/posts/${post.id}`}
onMouseEnter={() => prefetchPost(post.id)}
>
{post.title}
</Link>
```
---
## API Service Pattern
### Centralized API Service
```typescript
// features/posts/api/postsApi.ts
import { apiClient } from '@/lib/apiClient';
import type { Post, CreatePostDto, UpdatePostDto } from '~/types/post';
export const postsApi = {
getAll: async (): Promise<Post[]> => {
const response = await apiClient.get('/posts');
return response.data;
},
get: async (id: string): Promise<Post> => {
const response = await apiClient.get(`/posts/${id}`);
return response.data;
},
create: async (data: CreatePostDto): Promise<Post> => {
const response = await apiClient.post('/posts', data);
return response.data;
},
update: async (id: string, data: UpdatePostDto): Promise<Post> => {
const response = await apiClient.put(`/posts/${id}`, data);
return response.data;
},
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/posts/${id}`);
},
getByUser: async (userId: string): Promise<Post[]> => {
const response = await apiClient.get(`/users/${userId}/posts`);
return response.data;
}
};
```
### Usage in Components
```typescript
import { postsApi } from '~/features/posts/api/postsApi';
// In query
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll
});
// In mutation
const mutation = useMutation({
mutationFn: postsApi.create
});
```
---
## Query Keys
### Key Structure
```typescript
// List queries
['posts'][('posts', { status: 'published' })][ // All posts // Filtered posts
// Detail queries
('posts', postId)
][('posts', postId, 'comments')][ // Single post // Post comments
// Nested resources
('users', userId, 'posts')
][('users', userId, 'posts', postId)]; // User's posts // Specific user post
```
### Key Factories
```typescript
// features/posts/api/postKeys.ts
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: string) => [...postKeys.lists(), { filters }] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
comments: (id: string) => [...postKeys.detail(id), 'comments'] as const
};
// Usage
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(postId),
queryFn: () => postsApi.get(postId)
});
// Invalidate all post lists
queryClient.invalidateQueries({ queryKey: postKeys.lists() });
```
---
## Error Handling
### With Error Boundaries
```typescript
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
// In component
function DataComponent() {
const { data } = useSuspenseQuery({
queryKey: ['data'],
queryFn: fetchData,
// Errors automatically caught by ErrorBoundary
});
return <div>{data}</div>;
}
```
### Retry and Cache Configuration
```typescript
const { data } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
retry: 3, // Retry 3 times
retryDelay: 1000, // Wait 1s between retries
gcTime: 5 * 60 * 1000 // Garbage collection time: 5 minutes (v5: was 'cacheTime')
});
```
---
## Best Practices
### 1. Use Suspense by Default
```typescript
// ✅ Good: useSuspenseQuery + Suspense
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
function DataComponent() {
const { data } = useSuspenseQuery({...});
return <div>{data}</div>;
}
// ❌ Avoid: useQuery with manual loading
function DataComponent() {
const { data, isPending } = useQuery({...});
if (isPending) return <Spinner />;
return <div>{data}</div>;
}
```
### 2. Consistent Query Keys
```typescript
// ✅ Good: Use key factories
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(id),
queryFn: () => postsApi.get(id)
});
// ❌ Avoid: Inconsistent keys
const { data } = useSuspenseQuery({
queryKey: ['post', id], // Different format
queryFn: () => postsApi.get(id)
});
```
### 3. Centralized API Services
```typescript
// ✅ Good: API service
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll
});
// ❌ Avoid: Inline fetching
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts');
return res.json();
}
});
```
---
## Additional Resources
For more patterns, see:
- [data-fetching.md](resources/data-fetching.md) - Advanced patterns
- [cache-strategies.md](resources/cache-strategies.md) - Cache management
- [mutation-patterns.md](resources/mutation-patterns.md) - Complex mutations

View File

@@ -0,0 +1,252 @@
# Cache Management Strategies
## Cache Time Configuration
```typescript
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
gcTime: 10 * 60 * 1000 // Keep in cache for 10 minutes (formerly cacheTime)
});
```
## Cache Invalidation
### Invalidate Specific Queries
```typescript
const queryClient = useQueryClient();
// Invalidate all post queries
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Invalidate specific post
queryClient.invalidateQueries({ queryKey: ['post', postId] });
// Invalidate with exact match
queryClient.invalidateQueries({
queryKey: ['posts'],
exact: true // Only ['posts'], not ['posts', 'list']
});
```
### Invalidate on Mutation
```typescript
const { mutate } = useMutation({
mutationFn: createPost,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
}
});
```
## Manual Cache Updates
### Set Query Data
```typescript
// Update cache directly
queryClient.setQueryData(['post', postId], (oldData) => ({
...oldData,
title: 'New Title'
}));
// Set new data
queryClient.setQueryData(['post', postId], newPost);
```
### Get Query Data
```typescript
// Read from cache
const cachedPost = queryClient.getQueryData(['post', postId]);
// Use in initialData
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
initialData: () => queryClient.getQueryData(['posts'])?.find((p) => p.id === postId)
});
```
## Refetch Strategies
### Refetch on Window Focus
```typescript
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnWindowFocus: true // Refetch when tab regains focus
});
```
### Refetch on Reconnect
```typescript
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnReconnect: true // Refetch when internet reconnects
});
```
### Refetch Intervals
```typescript
const { data } = useQuery({
queryKey: ['live-data'],
queryFn: fetchLiveData,
refetchInterval: 5000, // Refetch every 5 seconds
refetchIntervalInBackground: false // Pause when tab not active
});
```
## Cache Persistence
### Persist to localStorage
```typescript
import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24 // 24 hours
}
}
});
const persister = createSyncStoragePersister({
storage: window.localStorage
});
<PersistQueryClientProvider
client={queryClient}
persister={persister}
>
<App />
</PersistQueryClientProvider>
```
## Cache Deduplication
### Automatic Request Deduplication
```typescript
// Both components will share the same request
function Component1() {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
}
function Component2() {
const { data } = useQuery({
queryKey: ['posts'], // Same key = same request
queryFn: fetchPosts
});
}
```
## Cache Preloading
### Prefetch Queries
```typescript
const queryClient = useQueryClient();
// Prefetch before navigation
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId)
});
};
// Prefetch in loader
router.beforeEach(async (to, from, next) => {
await queryClient.prefetchQuery({
queryKey: ['user', to.params.userId],
queryFn: () => fetchUser(to.params.userId)
});
next();
});
```
### Ensure Query Data
```typescript
// Fetch if not in cache, otherwise use cached
await queryClient.ensureQueryData({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId)
});
```
## Selective Cache Updates
### Update Nested Data
```typescript
queryClient.setQueryData(['posts'], (oldPosts) => {
return oldPosts.map((post) => (post.id === updatedPost.id ? updatedPost : post));
});
```
### Add to List Cache
```typescript
// After creating a post
queryClient.setQueryData(['posts'], (oldPosts = []) => {
return [newPost, ...oldPosts];
});
```
### Remove from List Cache
```typescript
// After deleting a post
queryClient.setQueryData(['posts'], (oldPosts) => {
return oldPosts.filter((post) => post.id !== deletedPostId);
});
```
## Cache Debugging
### React Query Devtools
```typescript
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
```
### Query Cache Events
```typescript
const queryCache = queryClient.getQueryCache();
queryCache.subscribe((event) => {
console.log('Query cache event:', event.type, event.query.queryKey);
});
```
## Best Practices
1. **Set Appropriate staleTime** - Balance freshness vs performance
2. **Use Invalidation Over Refetch** - Let queries refetch when needed
3. **Prefetch Predictably** - Preload data on hover/intent
4. **Update Cache on Mutations** - Keep UI in sync
5. **Use Devtools** - Debug cache issues visually
6. **Persist Important Data** - Save to localStorage for offline support
7. **Deduplicate Requests** - Rely on automatic deduplication

View File

@@ -0,0 +1,240 @@
# Advanced Data Fetching Patterns with TanStack Query
## Dependent Queries
Queries that depend on data from other queries:
```typescript
// First query - Get user ID
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchCurrentUser
});
// Second query - Depends on user ID
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchUserPosts(user!.id),
enabled: !!user // Only run when user is available
});
```
## Parallel Queries
Fetch multiple independent queries simultaneously:
```typescript
function Dashboard() {
const queries = useQueries({
queries: [
{ queryKey: ['stats'], queryFn: fetchStats },
{ queryKey: ['recentPosts'], queryFn: fetchRecentPosts },
{ queryKey: ['notifications'], queryFn: fetchNotifications }
]
});
const [statsQuery, postsQuery, notificationsQuery] = queries;
if (queries.some(q => q.isLoading)) return <Loading />;
return <Dashboard
stats={statsQuery.data}
posts={postsQuery.data}
notifications={notificationsQuery.data}
/>;
}
```
## Infinite Queries
For pagination and infinite scroll:
```typescript
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
initialPageParam: 1
});
// Flatten pages
const allPosts = data?.pages.flatMap(page => page.posts) ?? [];
return (
<div>
{allPosts.map(post => <PostCard key={post.id} post={post} />)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
```
## Prefetching
Preload data before it's needed:
```typescript
import { useQueryClient } from '@tanstack/react-query';
function PostLink({ postId }: { postId: string }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId)
});
};
return (
<Link to={`/posts/${postId}`} onMouseEnter={handleMouseEnter}>
View Post
</Link>
);
}
```
## Suspense Mode
Use with React Suspense:
```typescript
import { useSuspenseQuery } from '@tanstack/react-query';
function PostDetails({ postId }: { postId: string }) {
// Throws promise on loading, error on error
const { data: post } = useSuspenseQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId)
});
return <div>{post.title}</div>;
}
// Wrap with Suspense
<Suspense fallback={<Loading />}>
<PostDetails postId="123" />
</Suspense>
```
## Query Cancellation
Cancel queries when component unmounts:
```typescript
const { data, isLoading } = useQuery({
queryKey: ['search', searchTerm],
queryFn: async ({ signal }) => {
const response = await fetch(`/api/search?q=${searchTerm}`, { signal });
return response.json();
}
});
```
## Initial Data
Provide initial data to avoid loading state:
```typescript
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
initialData: () => {
// Get from cache or other source
return queryClient.getQueryData(['posts'])?.find((post) => post.id === postId);
}
});
```
## Placeholder Data
Show placeholder while loading:
```typescript
const { data, isPlaceholderData } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: (previousData) => previousData // Keep previous page while loading
});
// Or provide static placeholder
placeholderData: { posts: [], total: 0 }
```
## Optimistic Updates with Queries
Update UI immediately, rollback on error:
```typescript
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
// Cancel outgoing queries
await queryClient.cancelQueries({ queryKey: ['post', newPost.id] });
// Snapshot current value
const previousPost = queryClient.getQueryData(['post', newPost.id]);
// Optimistically update
queryClient.setQueryData(['post', newPost.id], newPost);
return { previousPost };
},
onError: (err, newPost, context) => {
// Rollback on error
queryClient.setQueryData(['post', newPost.id], context?.previousPost);
},
onSettled: (newPost) => {
// Refetch after success or error
queryClient.invalidateQueries({ queryKey: ['post', newPost.id] });
}
});
```
## Query Retries
Configure retry behavior:
```typescript
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
retry: 3, // Retry 3 times
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff
});
```
## Error Handling
Handle query errors:
```typescript
const { data, error, isError } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
throwOnError: false // Don't throw, just set error
});
if (isError) {
return <ErrorMessage error={error} />;
}
```
## Best Practices
1. **Use Suspense** - Better loading UX with React Suspense
2. **Prefetch on Intent** - Preload data on hover/focus
3. **Enable Queries Conditionally** - Use `enabled` option
4. **Cancel on Unmount** - Use abort signals
5. **Handle Errors Gracefully** - Show error states
6. **Optimize with Placeholders** - Show previous data while loading

View File

@@ -0,0 +1,344 @@
# Complex Mutation Patterns
## Basic Mutations
```typescript
const { mutate, isPending, isError, error } = useMutation({
mutationFn: (newPost: CreatePostDto) => createPost(newPost),
onSuccess: (data) => {
console.log('Post created:', data);
},
onError: (error) => {
console.error('Failed to create post:', error);
}
});
// Trigger mutation
mutate({ title: 'New Post', content: '...' });
```
## Optimistic Updates
Update UI immediately, rollback on error:
```typescript
const { mutate } = useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts'] });
// Snapshot previous value
const previousPosts = queryClient.getQueryData(['posts']);
// Optimistically update to the new value
queryClient.setQueryData(['posts'], (old) =>
old.map((post) => (post.id === newPost.id ? newPost : post))
);
// Return context with snapshot
return { previousPosts };
},
onError: (err, newPost, context) => {
// Rollback to previous value
queryClient.setQueryData(['posts'], context.previousPosts);
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['posts'] });
}
});
```
## Sequential Mutations
Run mutations in sequence:
```typescript
const createAndPublish = async (postData) => {
// Create post
const post = await createPostMutation.mutateAsync(postData);
// Publish post
const published = await publishPostMutation.mutateAsync(post.id);
return published;
};
```
## Parallel Mutations
Run multiple mutations simultaneously:
```typescript
const { mutate } = useMutation({
mutationFn: async (updates) => {
const results = await Promise.all([
updateProfile(updates.profile),
updateSettings(updates.settings),
updatePreferences(updates.preferences)
]);
return results;
}
});
```
## Mutation with Invalidation
```typescript
const { mutate } = useMutation({
mutationFn: createPost,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Or update cache directly
queryClient.setQueryData(['posts'], (old) => [newPost, ...old]);
}
});
```
## Mutation with Multiple Cache Updates
```typescript
const { mutate } = useMutation({
mutationFn: deletePost,
onSuccess: (_, deletedPostId) => {
// Update posts list
queryClient.setQueryData(['posts'], (old) => old.filter((post) => post.id !== deletedPostId));
// Update post count
queryClient.setQueryData(['postsCount'], (old) => old - 1);
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ['user', 'stats'] });
}
});
```
## Error Handling
```typescript
const { mutate, isError, error, reset } = useMutation({
mutationFn: createPost,
onError: (error) => {
if (error.code === 'VALIDATION_ERROR') {
setFormErrors(error.fields);
} else if (error.code === 'NETWORK_ERROR') {
showRetryDialog();
} else {
showGenericError();
}
}
});
// Clear error state
reset();
```
## Retry Failed Mutations
```typescript
const { mutate } = useMutation({
mutationFn: createPost,
retry: 3, // Retry 3 times on failure
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff
});
```
## Mutation with Loading State
```typescript
function CreatePostForm() {
const { mutate, isPending } = useMutation({
mutationFn: createPost,
onSuccess: () => {
navigate('/posts');
}
});
const handleSubmit = (data) => {
mutate(data);
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
```
## Mutation with Variables
```typescript
const { mutate, variables } = useMutation({
mutationFn: updatePost
});
// Access last mutation variables
console.log('Last updated post:', variables);
```
## Mutation Callbacks
```typescript
const { mutate } = useMutation({
mutationFn: createPost,
onMutate: (variables) => {
console.log('Starting mutation with:', variables);
},
onSuccess: (data, variables, context) => {
console.log('Success!', data);
},
onError: (error, variables, context) => {
console.error('Error!', error);
},
onSettled: (data, error, variables, context) => {
console.log('Mutation finished (success or error)');
}
});
```
## Mutation with Form Integration
```typescript
import { useForm } from 'react-hook-form';
function CreatePostForm() {
const { register, handleSubmit, reset } = useForm();
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createPost,
onSuccess: () => {
reset(); // Clear form
toast.success('Post created!');
},
onError: (error) => {
toast.error(error.message);
}
});
const onSubmit = (data) => {
mutate(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title')} />
<textarea {...register('content')} />
<button type="submit" disabled={isPending}>
Submit
</button>
{isError && <ErrorMessage error={error} />}
</form>
);
}
```
## Mutation State Reset
```typescript
const { mutate, data, error, reset } = useMutation({
mutationFn: createPost
});
// Clear mutation state
const handleReset = () => {
reset(); // Clears data, error, status, etc.
};
```
## Global Mutation Configuration
```typescript
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: (error) => {
// Global error handler
console.error('Mutation error:', error);
}
}
}
});
```
## Mutation Lifecycle
```
┌─────────────┐
│ idle │
└──────┬──────┘
│ mutate()
┌─────────────┐
│ pending │ ─── onMutate()
└──────┬──────┘
├─ success ──► onSuccess() ──┐
│ │
└─ error ────► onError() ────┤
onSettled()
```
## Best Practices
1. **Use Optimistic Updates** - Better UX for fast operations
2. **Always Handle Errors** - Show clear error messages
3. **Invalidate Related Queries** - Keep cache in sync
4. **Use onSettled** - For cleanup that runs regardless of success/error
5. **Reset on Unmount** - Clear mutation state when component unmounts
6. **Retry Network Errors** - Configure retry for transient failures
7. **Show Loading States** - Disable buttons during mutation
8. **Rollback on Error** - Revert optimistic updates if mutation fails
## Common Patterns
### Create with Redirect
```typescript
const { mutate } = useMutation({
mutationFn: createPost,
onSuccess: (newPost) => {
navigate(`/posts/${newPost.id}`);
}
});
```
### Update with Toast
```typescript
const { mutate } = useMutation({
mutationFn: updatePost,
onSuccess: () => {
toast.success('Post updated!');
},
onError: () => {
toast.error('Failed to update post');
}
});
```
### Delete with Confirmation
```typescript
const { mutate } = useMutation({
mutationFn: deletePost,
onMutate: async () => {
const confirmed = await confirm('Are you sure?');
if (!confirmed) throw new Error('Cancelled');
},
onSuccess: () => {
toast.success('Post deleted');
navigate('/posts');
}
});
```

View File

@@ -0,0 +1,86 @@
{
"tanstack-query": {
"type": "domain",
"enforcement": "suggest",
"priority": "high",
"promptTriggers": {
"keywords": [
"tanstack query",
"react query",
"@tanstack/react-query",
"useQuery",
"useMutation",
"useSuspenseQuery",
"useInfiniteQuery",
"useQueries",
"useQueryClient",
"useIsFetching",
"useIsMutating",
"useMutationState",
"QueryClient",
"QueryClientProvider",
"queryKey",
"queryFn",
"mutationFn",
"invalidateQueries",
"setQueryData",
"getQueryData",
"removeQueries",
"resetQueries",
"staleTime",
"cacheTime",
"refetchInterval",
"refetchOnWindowFocus",
"refetchOnReconnect",
"isLoading",
"isError",
"isFetching",
"isSuccess",
"queryCache",
"mutationCache"
],
"intentPatterns": [
"use.*tanstack.*query",
"use.*react.*query",
"create.*tanstack.*(query|mutation)",
"invalidate.*query.*cache",
"invalidate.*tanstack.*queries",
"prefetch.*query",
"setup.*query.*client",
"configure.*tanstack.*query",
"use.*(useQuery|useMutation|useInfiniteQuery)",
"implement.*query.*invalidation",
"add.*query.*key",
"set.*query.*data"
]
},
"fileTriggers": {
"pathPatterns": [
"**/api/**/*.ts",
"**/api/**/*.tsx",
"**/queries/**/*.ts",
"**/queries/**/*.tsx",
"**/hooks/**/*.ts",
"**/hooks/**/*.tsx",
"**/mutations/**/*.ts",
"**/mutations/**/*.tsx"
],
"contentPatterns": [
"useQuery\\(",
"useMutation\\(",
"useSuspenseQuery\\(",
"useInfiniteQuery\\(",
"useQueryClient\\(",
"queryClient\\.",
"import.*@tanstack/react-query",
"from '@tanstack/react-query'",
"QueryClientProvider",
"new QueryClient\\(",
"invalidateQueries\\(",
"setQueryData\\(",
"queryKey:",
"queryFn:"
]
}
}
}