Manages server state with TanStack Query (React Query) including data fetching, caching, mutations, and optimistic updates. Use when fetching API data, caching responses, handling loading states, or syncing server state.
Manages server state with TanStack Query for data fetching, caching, and mutations. Use when building React apps that fetch API data, handle loading states, or need optimistic updates.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Powerful data-fetching and server state management library for React applications.
Install:
npm install @tanstack/react-query
Setup Provider:
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
import { useQuery } from '@tanstack/react-query';
function Posts() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('/api/posts');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
},
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
function Post({ id }: { id: string }) {
const { data, isLoading } = useQuery({
queryKey: ['posts', id],
queryFn: async () => {
const response = await fetch(`/api/posts/${id}`);
return response.json();
},
});
if (isLoading) return <div>Loading...</div>;
return <h1>{data.title}</h1>;
}
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Timing
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
gcTime: 10 * 60 * 1000, // Cache garbage collected after 10 minutes
refetchInterval: 30 * 1000, // Refetch every 30 seconds
// Behavior
enabled: !!userId, // Only run if userId exists
retry: 3, // Retry failed requests 3 times
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
// Refetch triggers
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
// Placeholder data
placeholderData: [], // Show while loading
initialData: cachedData, // Use cached data initially
});
function UserPosts({ userId }: { userId: string }) {
// First query
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// Dependent query - only runs when user exists
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPostsByUser(user!.id),
enabled: !!user, // Only run when user is available
});
return <div>{/* ... */}</div>;
}
import { useQueries } from '@tanstack/react-query';
function Dashboard() {
const results = useQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['posts'], queryFn: fetchPosts },
{ queryKey: ['comments'], queryFn: fetchComments },
],
});
const isLoading = results.some((result) => result.isLoading);
const [users, posts, comments] = results.map((r) => r.data);
if (isLoading) return <div>Loading...</div>;
return <div>{/* Use users, posts, comments */}</div>;
}
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newPost: { title: string; content: string }) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return response.json();
},
onSuccess: () => {
// Invalidate and refetch posts
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
title: formData.get('title') as string,
content: formData.get('content') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" required />
<textarea name="content" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
</form>
);
}
const mutation = useMutation({
mutationFn: createPost,
// Called before mutation
onMutate: async (newPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts'] });
// Snapshot previous value
const previousPosts = queryClient.getQueryData(['posts']);
// Optimistically update
queryClient.setQueryData(['posts'], (old) => [...old, newPost]);
// Return context for rollback
return { previousPosts };
},
// Called on error
onError: (err, newPost, context) => {
// Rollback on error
queryClient.setQueryData(['posts'], context?.previousPosts);
},
// Called on success or error
onSettled: () => {
// Always refetch after mutation
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
// Called on success only
onSuccess: (data, variables, context) => {
console.log('Created:', data);
},
});
function TodoItem({ todo }: { todo: Todo }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (completed: boolean) =>
updateTodo(todo.id, { completed }),
onMutate: async (completed) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.map((t) =>
t.id === todo.id ? { ...t, completed } : t
)
);
return { previousTodos };
},
onError: (err, completed, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => mutation.mutate(e.target.checked)}
/>
);
}
// Simple key
['posts']
// With ID
['posts', postId]
// With filters
['posts', { status: 'published', author: userId }]
// Nested structure
['users', userId, 'posts', { page, limit }]
// Invalidate exact match
queryClient.invalidateQueries({ queryKey: ['posts', 1] });
// Invalidate all posts queries
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Invalidate with predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'posts' && query.state.data?.length > 0,
});
// Set data directly
queryClient.setQueryData(['posts', id], newPost);
// Update with function
queryClient.setQueryData(['posts'], (old) => [...old, newPost]);
// Get cached data
const posts = queryClient.getQueryData(['posts']);
// Remove from cache
queryClient.removeQueries({ queryKey: ['posts'] });
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Invalidate without refetch
queryClient.invalidateQueries({
queryKey: ['posts'],
refetchType: 'none',
});
// Invalidate inactive queries too
queryClient.invalidateQueries({
queryKey: ['posts'],
refetchType: 'all',
});
// Prefetch on hover
function PostLink({ id }: { id: string }) {
const queryClient = useQueryClient();
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['posts', id],
queryFn: () => fetchPost(id),
staleTime: 5 * 60 * 1000,
});
};
return (
<Link href={`/posts/${id}`} onMouseEnter={prefetch}>
View Post
</Link>
);
}
// Prefetch in loader
export async function loader() {
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
return null;
}
import { useInfiniteQuery } from '@tanstack/react-query';
function InfinitePosts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }) => {
const response = await fetch(`/api/posts?cursor=${pageParam}`);
return response.json();
},
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading...'
: hasNextPage
? 'Load More'
: 'No more posts'}
</button>
</div>
);
}
import { useSuspenseQuery } from '@tanstack/react-query';
import { Suspense } from 'react';
function Posts() {
// This will suspend until data is ready
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts />
</Suspense>
);
}
const api = {
posts: {
list: async () => {
const res = await fetch('/api/posts');
return res.json();
},
get: async (id: string) => {
const res = await fetch(`/api/posts/${id}`);
return res.json();
},
create: async (data: CreatePostInput) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
},
},
};
// Usage
const { data } = useQuery({
queryKey: ['posts'],
queryFn: api.posts.list,
});
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
};
// Usage
useQuery({
queryKey: postKeys.detail(id),
queryFn: () => api.posts.get(id),
});
// Invalidate all posts
queryClient.invalidateQueries({ queryKey: postKeys.all });
// Invalidate only lists
queryClient.invalidateQueries({ queryKey: postKeys.lists() });
// hooks/usePosts.ts
export function usePosts(filters?: PostFilters) {
return useQuery({
queryKey: postKeys.list(filters ?? {}),
queryFn: () => api.posts.list(filters),
});
}
export function usePost(id: string) {
return useQuery({
queryKey: postKeys.detail(id),
queryFn: () => api.posts.get(id),
enabled: !!id,
});
}
export function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.posts.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: postKeys.lists() });
},
});
}
// Usage
function PostList() {
const { data: posts, isLoading } = usePosts({ status: 'published' });
const createPost = useCreatePost();
// ...
}
// app/posts/page.tsx
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getQueryClient } from '@/lib/query-client';
import { PostList } from '@/components/PostList';
export default async function PostsPage() {
const queryClient = getQueryClient();
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
);
}
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
import { cache } from 'react';
export const getQueryClient = cache(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
);
| Mistake | Fix |
|---|---|
| Inline query functions | Extract to named functions |
| Missing error handling | Always handle isError |
| Stale closures in callbacks | Use functional updates |
| Not invalidating after mutation | Call invalidateQueries |
| Incorrect query key dependencies | Include all variables in key |
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.