| 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:
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>
);
}
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>
Benefits:
- No
isLoading checks 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),
});
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 }) {
const { data: post } = useSuspenseQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
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
useSuspenseQuery by default (preferred)
- Use
useQuery only 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: () => {
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) => {
await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });
const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);
queryClient.setQueryData(['posts', updatedPost.id], updatedPost);
return { previousPost };
},
onError: (err, updatedPost, context) => {
queryClient.setQueryData(['posts', updatedPost.id], context.previousPost);
},
onSettled: (data, error, variables) => {
queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });
}
});
Cache Management
Invalidation
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries({ queryKey: ['posts', postId] });
queryClient.invalidateQueries();
Manual Updates
queryClient.setQueryData(['posts', postId], newPost);
queryClient.setQueryData(['posts'], (oldPosts) => [...oldPosts, newPost]);
Prefetching
await queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
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
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';
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll
});
const mutation = useMutation({
mutationFn: postsApi.create
});
Query Keys
Key Structure
['posts'][('posts', { status: 'published' })][
('posts', postId)
][('posts', postId, 'comments')][
('users', userId, 'posts')
][('users', userId, 'posts', postId)];
Key Factories
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
};
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(postId),
queryFn: () => postsApi.get(postId)
});
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>
function DataComponent() {
const { data } = useSuspenseQuery({
queryKey: ['data'],
queryFn: fetchData,
});
return <div>{data}</div>;
}
Retry and Cache Configuration
const { data } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
retry: 3,
retryDelay: 1000,
gcTime: 5 * 60 * 1000
});
Best Practices
1. Use Suspense by Default
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
function DataComponent() {
const { data } = useSuspenseQuery({...});
return <div>{data}</div>;
}
function DataComponent() {
const { data, isPending } = useQuery({...});
if (isPending) return <Spinner />;
return <div>{data}</div>;
}
2. Consistent Query Keys
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(id),
queryFn: () => postsApi.get(id)
});
const { data } = useSuspenseQuery({
queryKey: ['post', id],
queryFn: () => postsApi.get(id)
});
3. Centralized API Services
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll
});
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts');
return res.json();
}
});
Additional Resources
For more patterns, see: