Agent skill
tanstack-query-advanced
Advanced TanStack Query v5 patterns for infinite queries, optimistic updates, prefetching, gcTime, and queryOptions. Use when building data fetching, caching, or optimistic updates.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/tanstack-query-advanced
SKILL.md
TanStack Query Advanced
Production patterns for TanStack Query v5 - server state management done right.
Overview
- Infinite scroll / pagination
- Optimistic UI updates
- Prefetching for instant navigation
- Complex cache invalidation
- Dependent/parallel queries
- Mutations with rollback
Core Patterns
1. Infinite Queries (Cursor-Based)
import { useInfiniteQuery } from '@tanstack/react-query';
interface Page {
items: Item[];
nextCursor: string | null;
}
function useInfiniteItems() {
return useInfiniteQuery({
queryKey: ['items'],
queryFn: async ({ pageParam }): Promise<Page> => {
const res = await fetch(`/api/items?cursor=${pageParam ?? ''}`);
return res.json();
},
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
});
}
// Component
function ItemList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteItems();
return (
<>
{data?.pages.flatMap((page) => page.items.map((item) => (
<ItemCard key={item.id} item={item} />
)))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
</button>
</>
);
}
2. Optimistic Updates
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useUpdateTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });
// Snapshot previous value
const previousTodo = queryClient.getQueryData(['todos', newTodo.id]);
// Optimistically update
queryClient.setQueryData(['todos', newTodo.id], newTodo);
// Return context for rollback
return { previousTodo };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos', newTodo.id], context?.previousTodo);
},
onSettled: (data, error, variables) => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos', variables.id] });
},
});
}
3. Prefetching Patterns
// Prefetch on hover
function UserLink({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
return (
<Link to={`/users/${userId}`} onMouseEnter={prefetchUser}>
View User
</Link>
);
}
// Prefetch in loader (React Router)
export const loader = (queryClient: QueryClient) => async ({ params }) => {
await queryClient.ensureQueryData({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id),
});
return null;
};
4. Smart Cache Invalidation
const queryClient = useQueryClient();
// Invalidate exact query
queryClient.invalidateQueries({ queryKey: ['todos', 1] });
// Invalidate all todos queries
queryClient.invalidateQueries({ queryKey: ['todos'] });
// Invalidate with predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
(query.queryKey[1] as Todo)?.status === 'done',
});
// Invalidate and refetch immediately
queryClient.refetchQueries({ queryKey: ['todos'], type: 'active' });
// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['todos', 1] });
5. Dependent Queries
function useUserPosts(userId: string) {
// First query
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// Dependent query - only runs when user is loaded
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!userQuery.data, // Only fetch when user exists
});
return { user: userQuery.data, posts: postsQuery.data };
}
6. Parallel Queries
import { useQueries } from '@tanstack/react-query';
function useMultipleUsers(userIds: string[]) {
return useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000,
})),
combine: (results) => ({
users: results.map((r) => r.data).filter(Boolean),
pending: results.some((r) => r.isPending),
error: results.find((r) => r.error)?.error,
}),
});
}
7. Query Deduplication & Batching
// Configure in QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 5, // 5 minutes (formerly cacheTime)
refetchOnWindowFocus: false,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
8. Suspense Integration
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
// This will suspend until data is ready
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <div>{user.name}</div>;
}
// Wrap with Suspense
<Suspense fallback={<Skeleton />}>
<UserProfile userId="123" />
</Suspense>
9. Mutation State Tracking
import { useMutationState } from '@tanstack/react-query';
function PendingTodos() {
// Track all pending todo mutations
const pendingMutations = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
return (
<>
{pendingMutations.map((todo) => (
<TodoItem key={todo.id} todo={todo} isPending />
))}
</>
);
}
Configuration Best Practices
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // Data fresh for 1 min
gcTime: 1000 * 60 * 5, // Cache for 5 min
refetchOnWindowFocus: true, // Refetch on tab focus
refetchOnReconnect: true, // Refetch on network reconnect
retry: 3, // Retry failed requests
},
mutations: {
retry: 1,
onError: (error) => toast.error(error.message),
},
},
});
Quick Reference
// ✅ Create typed query with queryOptions helper (v5)
const userQueryOptions = (id: string) => queryOptions({
queryKey: ['user', id] as const,
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000,
});
// ✅ Use the query options for consistency
const { data } = useQuery(userQueryOptions(userId));
await queryClient.prefetchQuery(userQueryOptions(userId));
await queryClient.ensureQueryData(userQueryOptions(userId));
// ✅ v5: isPending instead of isLoading (initial load only)
if (isPending) return <Skeleton />;
// ✅ v5: gcTime instead of cacheTime
gcTime: 5 * 60 * 1000,
// ✅ useSuspenseQuery for Suspense integration
const { data } = useSuspenseQuery(userQueryOptions(userId));
// ✅ Selective invalidation
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });
// ❌ NEVER destructure useQuery result at call site
const { data, isLoading } = useQuery({ queryKey: ['users'] }); // BAD - recreates object
// ❌ NEVER use string keys
useQuery({ queryKey: 'users' }); // BAD - use arrays
// ❌ NEVER store server state in Zustand
const useStore = create((set) => ({ users: [] })); // BAD - use React Query
Key Decisions
| Decision | Option A | Option B | Recommendation |
|---|---|---|---|
| Query key structure | String | Array | Array - supports hierarchy and serialization |
| Cache timing | staleTime | gcTime | Both - staleTime for freshness, gcTime for memory |
| Loading state | isLoading | isPending | isPending (v5) - isLoading includes background refetches |
| Query definition | Inline | queryOptions | queryOptions - reusable for prefetch/loader/useQuery |
| Suspense | useQuery + loading | useSuspenseQuery | useSuspenseQuery for React 18+ Suspense |
| Optimistic updates | setQueryData only | setQueryData + invalidate | Both - optimistic then reconcile |
| Parallel queries | Multiple useQuery | useQueries | useQueries - combined loading/error state |
| Infinite queries | Manual pagination | useInfiniteQuery | useInfiniteQuery - built-in cursor handling |
Anti-Patterns (FORBIDDEN)
// ❌ FORBIDDEN: Storing server state in Zustand/Redux
const useStore = create((set) => ({
users: [], // Server state belongs in React Query!
fetchUsers: async () => {
const users = await api.getUsers();
set({ users }); // Stale data, no background refetch
},
}));
// ❌ FORBIDDEN: String query keys
useQuery({
queryKey: 'todos', // Must be an array!
queryFn: fetchTodos,
});
// ❌ FORBIDDEN: Using deprecated cacheTime (v5)
useQuery({
queryKey: ['todos'],
cacheTime: 5 * 60 * 1000, // WRONG - use gcTime in v5
});
// ❌ FORBIDDEN: Using isLoading for initial state (v5)
// isLoading = isPending && isFetching (includes background refetch)
if (isLoading) return <Skeleton />; // WRONG - use isPending
// ❌ FORBIDDEN: Over-invalidating after mutations
useMutation({
mutationFn: updateTodo,
onSuccess: () => {
queryClient.invalidateQueries(); // Invalidates EVERYTHING!
},
});
// ❌ FORBIDDEN: Forgetting to cancel queries in optimistic updates
useMutation({
onMutate: async (newTodo) => {
// Missing: await queryClient.cancelQueries(...)
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
return { previous };
},
});
// ❌ FORBIDDEN: Not returning context from onMutate
useMutation({
onMutate: async (newTodo) => {
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// Missing: return { previous }; // Required for rollback!
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previous); // context is undefined!
},
});
// ❌ FORBIDDEN: Mutating cache data directly
queryClient.setQueryData(['todos'], (old) => {
old.push(newTodo); // WRONG - mutates existing array
return old;
});
// ✅ CORRECT: Return new array
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// ❌ FORBIDDEN: Fetching inside useEffect
useEffect(() => {
fetch('/api/users').then(setUsers); // Use React Query instead!
}, []);
Related Skills
zustand-patterns- Client state management (use alongside React Query for server state)form-state-patterns- Form state with React Hook Form (integrate mutation status)msw-mocking- Mock Service Worker for testing queries without networkreact-server-components-framework- RSC hydration with React Query
Capability Details
infinite-queries
Keywords: infinite, pagination, cursor, load more, scroll, pages Solves: Implementing cursor-based pagination with automatic page management
optimistic-updates
Keywords: optimistic, instant, rollback, onMutate, setQueryData, cancel Solves: Showing immediate UI feedback before server confirmation with rollback
prefetching
Keywords: prefetch, hover, preload, ensureQueryData, loader, navigation Solves: Loading data before it's needed for instant navigation
cache-invalidation
Keywords: invalidate, refetch, stale, fresh, gcTime, staleTime, exact Solves: Keeping cache in sync with server after mutations
suspense-integration
Keywords: suspense, useSuspenseQuery, streaming, fallback, boundary Solves: Integrating with React Suspense for declarative loading states
parallel-queries
Keywords: useQueries, parallel, concurrent, combine, batch Solves: Fetching multiple independent queries with combined state
References
references/cache-strategies.md- Cache invalidation patternstemplates/query-hooks-template.ts- Production query hook templatechecklists/tanstack-checklist.md- Implementation checklistexamples/tanstack-examples.md- Real-world usage examples
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?