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.
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
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→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 post
Key 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
More by blencorp
View allExpress.js framework patterns including routing, middleware, request/response handling, and Express-specific APIs. Use when working with Express routes, middleware, or Express applications.
Next.js 15+ App Router development patterns including Server Components, Client Components, data fetching, layouts, and server actions. Use when creating pages, routes, layouts, components, API route handlers, server actions, loading states, error boundaries, or working with Next.js navigation and metadata.
TanStack Router file-based routing patterns including route creation, navigation, loaders, type-safe routing, and lazy loading. Use when creating routes, implementing navigation, or working with TanStack Router.
Tailwind CSS v4 utility-first styling patterns including responsive design, dark mode, and custom configuration. Use when styling with Tailwind, adding utility classes, configuring Tailwind, setting up dark mode, or customizing the theme.
