
Tanstack Query
- 48 installs
- 101 repo stars
- Updated November 28, 2025
- blencorp/claude-code-kit
TanStack Query is a Claude Code skill that provides TanStack Query v5 patterns for data fetching, mutations, and cache management in React.
About
TanStack Query is a skill that gives Claude TanStack Query v5 data fetching patterns including useSuspenseQuery, useQuery, mutations, and cache management. A developer uses it when fetching data, managing server state, or working with TanStack Query hooks. It defaults to Suspense-based queries and documents the v5 breaking changes from v4.
- TanStack Query v5 data fetching with useSuspenseQuery as the default
- Mutations with optimistic updates and cache invalidation
- Documents v5 breaking changes from v4 (isPending, gcTime, removed callbacks)
Tanstack Query by the numbers
- 48 all-time installs (skills.sh)
- Ranked #1,319 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
tanstack-query capabilities & compatibility
- Capabilities
- frontend · data fetching · state management
- Use cases
- frontend · api development
- IDEs
- vscode · cursor ide
What tanstack-query says it does
TanStack Query v5 data fetching patterns including useSuspenseQuery, useQuery, mutations, cache management, and API service integration.
Modern data fetching with TanStack Query v5 (latest: 5.90.5, November 2025), emphasizing Suspense-based queries, cache-first strategies, and centralized API services.
For **all new components**, use `useSuspenseQuery`:
npx skills add https://github.com/blencorp/claude-code-kit --skill tanstack-queryAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 48 |
|---|---|
| repo stars | ★ 101 |
| Last updated | November 28, 2025 |
| Repository | blencorp/claude-code-kit ↗ |
What it does
Fetch data and manage server state in React with TanStack Query v5 hooks.
Who is it for?
Fetching data and managing server state in React with TanStack Query
Skip if: Non-React state or server-only data access
When should I use this skill?
Fetching data, managing server state, or working with TanStack Query hooks
What you get
Data fetching uses Suspense-based queries and correct v5 mutation and cache APIs.
- Query hooks
- Mutations
- Cache management logic
By the numbers
- 5 documented v5 breaking changes from v4
Files
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→isPendingfor statuscacheTime→gcTime(garbage collection time)- React 18.0+ required
- Callbacks removed from useQuery (onError, onSuccess, onSettled)
keepPreviousDatareplaced withplaceholderDatafunction
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:
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
isLoadingchecks needed - Integrates with Suspense boundaries
- Cleaner component code
- Consistent loading UX
---
useSuspenseQuery Patterns
Basic Usage
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => userApi.get(userId),
});
// data is never undefined - guaranteed by Suspense
return <div>{data.name}</div>;With Parameters
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
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:
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
useSuspenseQueryby default (preferred) - Use
useQueryonly when you need component-level loading states - Most cases should use
useSuspenseQuery+ Suspense boundaries
---
Mutations
Basic Mutation
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
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
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
// Update cache directly
queryClient.setQueryData(['posts', postId], newPost);
// Update with function
queryClient.setQueryData(['posts'], (oldPosts) => [
...oldPosts,
newPost,
]);Prefetching
// 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
// 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
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
// List queries
['posts'] // All posts
['posts', { status: 'published' }] // Filtered posts
// Detail queries
['posts', postId] // Single post
['posts', postId, 'comments'] // Post comments
// Nested resources
['users', userId, 'posts'] // User's posts
['users', userId, 'posts', postId] // Specific user postKey Factories
// 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
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
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
// ✅ 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
// ✅ 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
// ✅ 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 - Advanced patterns
- cache-strategies.md - Cache management
- mutation-patterns.md - Complex mutations
Cache Management Strategies
Cache Time Configuration
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
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
const { mutate } = useMutation({
mutationFn: createPost,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
}
});Manual Cache Updates
Set Query Data
// Update cache directly
queryClient.setQueryData(['post', postId], (oldData) => ({
...oldData,
title: 'New Title'
}));
// Set new data
queryClient.setQueryData(['post', postId], newPost);Get Query Data
// 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
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnWindowFocus: true // Refetch when tab regains focus
});Refetch on Reconnect
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnReconnect: true // Refetch when internet reconnects
});Refetch Intervals
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
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
// 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
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
// Fetch if not in cache, otherwise use cached
await queryClient.ensureQueryData({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId)
});Selective Cache Updates
Update Nested Data
queryClient.setQueryData(['posts'], (oldPosts) => {
return oldPosts.map(post =>
post.id === updatedPost.id ? updatedPost : post
);
});Add to List Cache
// After creating a post
queryClient.setQueryData(['posts'], (oldPosts = []) => {
return [newPost, ...oldPosts];
});Remove from List Cache
// After deleting a post
queryClient.setQueryData(['posts'], (oldPosts) => {
return oldPosts.filter(post => post.id !== deletedPostId);
});Cache Debugging
React Query Devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>Query Cache Events
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
Advanced Data Fetching Patterns with TanStack Query
Dependent Queries
Queries that depend on data from other queries:
// 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
Complex Mutation Patterns
Basic Mutations
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:
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:
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:
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
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
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
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
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
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
const { mutate, variables } = useMutation({
mutationFn: updatePost
});
// Access last mutation variables
console.log('Last updated post:', variables);Mutation Callbacks
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
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
const { mutate, data, error, reset } = useMutation({
mutationFn: createPost
});
// Clear mutation state
const handleReset = () => {
reset(); // Clears data, error, status, etc.
};Global Mutation Configuration
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
const { mutate } = useMutation({
mutationFn: createPost,
onSuccess: (newPost) => {
navigate(`/posts/${newPost.id}`);
}
});Update with Toast
const { mutate } = useMutation({
mutationFn: updatePost,
onSuccess: () => {
toast.success('Post updated!');
},
onError: () => {
toast.error('Failed to update post');
}
});Delete with Confirmation
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');
}
});{
"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:"
]
}
}
}
Related skills
FAQ
Which query hook is preferred?
useSuspenseQuery is the default for new components, with useQuery used only when component-level loading states are needed.
What changed in v5?
isLoading became isPending, cacheTime became gcTime, and useQuery callbacks like onError and onSuccess were removed.