From tanstack
Implements TanStack Query v5 in React apps for API data fetching, server state caching, mutations, optimistic updates, infinite scroll, streaming AI responses, and tRPC v11 integration.
npx claudepluginhub tenequm/claude-pluginsThis skill uses the workspace's default tool permissions.
Powerful asynchronous state management for React. TanStack Query makes fetching, caching, synchronizing, and updating server state in your React applications a breeze.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Powerful asynchronous state management for React. TanStack Query makes fetching, caching, synchronizing, and updating server state in your React applications a breeze.
npm install @tanstack/react-query
# or
pnpm add @tanstack/react-query
# or
yarn add @tanstack/react-query
Wrap your application with QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
import { useQuery } from '@tanstack/react-query';
function TodoList() {
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('https://api.example.com/todos');
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('https://api.example.com/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
headers: { 'Content-Type': 'application/json' },
});
return res.json();
},
onSuccess: () => {
// Invalidate and refetch todos
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button onClick={() => mutation.mutate({ title: 'New Todo' })}>
{mutation.isPending ? 'Creating...' : 'Create Todo'}
</button>
);
}
Query keys uniquely identify queries and are used for caching. They must be arrays.
// Simple key
useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// Key with variables
useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId) });
// Hierarchical keys
useQuery({ queryKey: ['todos', 'list', { filters, page }], queryFn: fetchTodos });
Query key matching:
['todos'] - exact match['todos', { page: 1 }] - exact match with object{ queryKey: ['todos'] } - matches all queries starting with 'todos'Query functions must return a promise that resolves data or throws an error:
// Using fetch
queryFn: async () => {
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
// Using axios
queryFn: () => axios.get(url).then(res => res.data)
// With query key access
queryFn: ({ queryKey }) => {
const [_, todoId] = queryKey;
return fetchTodo(todoId);
}
Understanding defaults is crucial for optimal usage:
// Override defaults globally
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 10, // 10 minutes
},
},
});
// Or per query
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60, // 1 minute
retry: 5,
});
Queries have two important states:
Query Status:
pending - No cached data, query is executingerror - Query encountered an errorsuccess - Query succeeded and data is availableFetch Status:
fetching - Query function is executingpaused - Query wants to fetch but is paused (offline)idle - Query is not fetchingconst { data, status, fetchStatus, isLoading, isFetching } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
// isLoading = status === 'pending'
// isFetching = fetchStatus === 'fetching'
Mark queries as stale to trigger refetches:
const queryClient = useQueryClient();
// Invalidate all todos queries
queryClient.invalidateQueries({ queryKey: ['todos'] });
// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['todo', todoId] });
// Invalidate and refetch immediately
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'active' // only refetch active queries
});
Mutations are used for creating, updating, or deleting data:
const mutation = useMutation({
mutationFn: (newTodo) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
},
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');
},
});
// Trigger mutation
mutation.mutate({ title: 'New Todo' });
// With async/await
mutation.mutateAsync({ title: 'New Todo' })
.then(data => console.log(data))
.catch(error => console.error(error));
TanStack Query supports React Suspense with dedicated hooks:
import { useSuspenseQuery } from '@tanstack/react-query';
function TodoList() {
// This will suspend the component until data is ready
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
// No need for loading states - handled by Suspense boundary
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
// In parent component
function App() {
return (
<Suspense fallback={<div>Loading todos...</div>}>
<TodoList />
</Suspense>
);
}
Consume AsyncIterable streams as query data - ideal for AI chat, SSE, and streaming responses:
import { useQuery, queryOptions } from '@tanstack/react-query';
import { experimental_streamedQuery as streamedQuery } from '@tanstack/react-query';
async function* fetchChatStream(sessionId: string): AsyncIterable<string> {
const response = await fetch(`/api/chat/${sessionId}`, { method: 'POST' });
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value);
}
}
function ChatMessages({ sessionId }: { sessionId: string }) {
const { data: chunks, status, fetchStatus } = useQuery(
queryOptions({
queryKey: ['chat', sessionId],
queryFn: streamedQuery({
streamFn: () => fetchChatStream(sessionId),
// Optional: customize how chunks accumulate
// reducer: (acc, chunk) => [...acc, chunk],
// initialValue: [],
refetchMode: 'reset', // 'reset' | 'append' | 'replace'
}),
})
);
// status === 'pending' until first chunk arrives
// status === 'success' after first chunk, fetchStatus === 'fetching' until stream ends
if (status === 'pending') return <div>Waiting for response...</div>;
return (
<div>
{chunks?.map((chunk, i) => <span key={i}>{chunk}</span>)}
{fetchStatus === 'fetching' && <span className="cursor" />}
</div>
);
}
refetchMode options:
'reset' - clear data and start fresh on refetch'append' - keep existing chunks and add new ones'replace' - replace data chunk-by-chunk on refetchNote: The API stabilized at v5.86.0. Earlier versions used queryFn instead of streamFn and maxChunks instead of reducer.
Use React 19's React.use() with TanStack Query for "render-as-you-fetch":
// Enable the feature flag
const queryClient = new QueryClient({
defaultOptions: {
queries: {
experimental_prefetchInRender: true,
},
},
});
// Component that suspends with React.use()
function TodoList({ query }: { query: UseQueryResult<Todo[]> }) {
const data = React.use(query.promise); // Suspends until resolved
return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>;
}
function App() {
const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return (
<React.Suspense fallback={<div>Loading...</div>}>
<TodoList query={query} />
</React.Suspense>
);
}
Known limitations: queries may run twice on unsuspend, incompatible with useQueries, skipToken and refetch() cannot be used together.
For detailed information on advanced patterns, see the reference files:
For implementing infinite scroll and load-more patterns:
references/infinite-queries.md for comprehensive guideuseInfiniteQuery hookgetNextPageParam and getPreviousPageParamFor updating UI before server confirmation:
references/optimistic-updates.md for detailed patternsFor full type safety and inference:
references/typescript.md for complete TypeScript guideFor advanced cache invalidation strategies:
references/query-invalidation.mdFor optimizing query performance:
references/performance.mdTanStack Query DevTools provide visual insights into query state:
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
DevTools features:
staleTime: Infinity ("static") queries (v5.80.0+)Run queries in sequence when one depends on another:
// First query
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// Second query depends on first
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user.id),
enabled: !!user?.id, // Only run when user.id is available
});
Multiple independent queries in one component:
function Dashboard() {
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
const stats = useQuery({ queryKey: ['stats'], queryFn: fetchStats });
if (users.isLoading || posts.isLoading || stats.isLoading) {
return <div>Loading...</div>;
}
// All queries succeeded
return <DashboardView users={users.data} posts={posts.data} stats={stats.data} />;
}
Use useQueries for dynamic number of queries:
import { useQueries } from '@tanstack/react-query';
function TodoLists({ listIds }) {
const results = useQueries({
queries: listIds.map((id) => ({
queryKey: ['list', id],
queryFn: () => fetchList(id),
})),
});
const isLoading = results.some(result => result.isLoading);
const data = results.map(result => result.data);
return <Lists data={data} />;
}
Prefetch data before it's needed:
const queryClient = useQueryClient();
// Prefetch on hover
function TodoListLink({ id }) {
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
staleTime: 1000 * 60 * 5, // Cache for 5 minutes
});
};
return (
<Link to={`/todo/${id}`} onMouseEnter={prefetch}>
View Todo
</Link>
);
}
Provide initial data to avoid loading states:
function TodoDetail({ todoId, initialTodo }) {
const { data } = useQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodo(todoId),
initialData: initialTodo, // Use this data immediately
staleTime: 1000 * 60, // Consider fresh for 1 minute
});
return <div>{data.title}</div>;
}
Show placeholder while loading:
const { data, isPlaceholderData } = useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: (previousData) => previousData, // Keep previous data while loading
});
// Or use static placeholder
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
placeholderData: { items: [], total: 0 },
});
// TypeScript: isPlaceholderData now narrows data type (v5.65.0+)
// When isPlaceholderData is true, data is typed as the placeholder type
tRPC v11 exposes queryOptions and mutationOptions directly, removing the need for custom hook wrappers:
import { useTRPC } from '@trpc/tanstack-react-query';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function TodoList() {
const trpc = useTRPC();
const queryClient = useQueryClient();
// Direct queryOptions pattern (replaces trpc.todo.list.useQuery())
const { data } = useQuery(trpc.todo.list.queryOptions());
const createTodo = useMutation(
trpc.todo.create.mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries(trpc.todo.list.queryOptions());
},
})
);
// Prefetching also works
queryClient.prefetchQuery(trpc.todo.list.queryOptions());
}
Requires @tanstack/react-query@5.62.8+ and @trpc/tanstack-react-query.
const { error, isError } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
if (isError) {
return <div>Error: {error.message}</div>;
}
Use QueryCache and MutationCache callbacks for global error handling:
import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
console.error(`Query ${query.queryKey} failed:`, error);
// Show toast notification, etc.
},
}),
mutationCache: new MutationCache({
onError: (error, _variables, _context, mutation) => {
console.error('Mutation failed:', error);
},
}),
});
Combine with React Error Boundaries:
import { useQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function TodoList() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true, // Throw errors to error boundary
});
return <div>{/* render data */}</div>;
}
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<TodoList />
</ErrorBoundary>
);
}
Use Query Keys Wisely
['todos', 'list', { filters }]Set Appropriate staleTime
staleTime: InfinitystaleTime: 0 (default)staleTime: 1000 * 60 * 5 (5 minutes)Handle Loading and Error States
isLoading and errorOptimize Refetching
refetchOnWindowFocus: falsestaleTime to reduce refetchesrefetchInterval for pollingInvalidate Efficiently
Use TypeScript
Leverage DevTools
If you're upgrading from React Query v4:
cacheTime renamed to gcTimeuseInfiniteQuery pageParam changesuseSuspenseQuery hooks