Claude-code-kit tanstack-query
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.
install
source · Clone the upstream repo
git clone https://github.com/Squirrelfishcityhall150/claude-code-kit
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Squirrelfishcityhall150/claude-code-kit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/cli/kits/tanstack-query/skills/tanstack-query" ~/.claude/skills/squirrelfishcityhall150-claude-code-kit-tanstack-query && rm -rf "$T"
manifest:
cli/kits/tanstack-query/skills/tanstack-query/SKILL.mdsource content
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
for statusisPending
→cacheTime
(garbage collection time)gcTime- React 18.0+ required
- Callbacks removed from useQuery (onError, onSuccess, onSettled)
replaced withkeepPreviousData
functionplaceholderData
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
checks neededisLoading - 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
vs useQuery
:useSuspenseQuery
- Use
by default (preferred)useSuspenseQuery - Use
only when you need component-level loading statesuseQuery - Most cases should use
+ Suspense boundariesuseSuspenseQuery
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