tanstack-query

TanStack Query Core Knowledge

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack-query" with this command: npx skills add claude-dev-suite/claude-dev-suite/claude-dev-suite-claude-dev-suite-tanstack-query

TanStack Query Core Knowledge

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: tanstack-query for comprehensive documentation.

Setup

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, // 1 minute retry: 1, }, }, });

function App() { return ( <QueryClientProvider client={queryClient}> <MyApp /> </QueryClientProvider> ); }

Queries

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) { const { data, isLoading, error } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), });

if (isLoading) return <Spinner />; if (error) return <Error message={error.message} />; return <div>{data.name}</div>; }

// With options const { data } = useQuery({ queryKey: ['users', { status: 'active' }], queryFn: () => fetchUsers({ status: 'active' }), staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, enabled: !!userId, // Conditional fetch });

Mutations

import { useMutation, useQueryClient } from '@tanstack/react-query';

function CreateUser() { const queryClient = useQueryClient();

const mutation = useMutation({ mutationFn: (newUser: CreateUserInput) => createUser(newUser), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); }, });

return ( <form onSubmit={(e) => { e.preventDefault(); mutation.mutate({ name: 'John' }); }}> <button disabled={mutation.isPending}> {mutation.isPending ? 'Creating...' : 'Create'} </button> </form> ); }

Optimistic Updates

const mutation = useMutation({ mutationFn: updateUser, onMutate: async (newData) => { await queryClient.cancelQueries({ queryKey: ['user', id] }); const previous = queryClient.getQueryData(['user', id]); queryClient.setQueryData(['user', id], newData); return { previous }; }, onError: (err, newData, context) => { queryClient.setQueryData(['user', id], context.previous); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['user', id] }); }, });

When NOT to Use This Skill

Scenario Use Instead

Client-side UI state (modals, form inputs) zustand or React state

Vue 3 applications pinia with custom composables

Static configuration data React Context API

Real-time WebSocket data Custom hooks with WebSocket + Zustand

GraphQL queries @apollo/client or urql

Anti-Patterns

Anti-Pattern Why It's Bad Correct Approach

Using queries for client state Unnecessary complexity, wrong abstraction Use Zustand or React state

Not using query keys properly Cache collisions, wrong data Use query key factory pattern

Fetching on every render Performance issues Set proper staleTime and gcTime

Manual cache invalidation everywhere Hard to maintain Use mutation's onSuccess with invalidateQueries

Ignoring loading/error states Poor UX Always handle isLoading and error

Using refetch() instead of invalidate()

Bypasses cache, wastes requests Use invalidateQueries() for revalidation

Not prefetching predictable navigation Slow perceived performance Prefetch on hover/mount

Storing queries in global state Defeats TanStack Query purpose Let TanStack Query manage cache

No retry strategy for transient errors Failed requests on network blips Configure retry with backoff

Missing query key dependencies Stale data when params change Include all variables in query key

Quick Troubleshooting

Issue Cause Solution

Queries not refetching staleTime too high or enabled: false

Lower staleTime or check enabled condition

"No QueryClient set" error Missing QueryClientProvider

Wrap app with <QueryClientProvider client={queryClient}>

Mutations not updating UI Not invalidating queries Call queryClient.invalidateQueries() in onSuccess

Infinite refetch loop Query key changes on every render Stabilize query key with useMemo or constants

SSR hydration mismatch Server/client data out of sync Use HydrationBoundary with dehydrated state

Memory leaks Queries never garbage collected Set proper gcTime (default 5 minutes)

Optimistic update reverted incorrectly Context not returned from onMutate

Return previous value from onMutate , restore in onError

Multiple identical requests No deduplication interval Set dedupingInterval in query options

Production Readiness

Query Client Configuration

// lib/queryClient.ts import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query';

export const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error, query) => { // Global error handling for queries if (query.state.data !== undefined) { // Only show error toasts for background refetch failures toast.error(Background update failed: ${error.message}); }

  // Log to monitoring
  logError({ type: 'query', key: query.queryKey, error });
},

}), mutationCache: new MutationCache({ onError: (error, _variables, _context, mutation) => { logError({ type: 'mutation', key: mutation.options.mutationKey, error }); }, }), defaultOptions: { queries: { staleTime: 1000 * 60, // 1 minute gcTime: 1000 * 60 * 5, // 5 minutes (formerly cacheTime) retry: (failureCount, error) => { // Don't retry on 4xx errors if (error.status >= 400 && error.status < 500) return false; return failureCount < 3; }, refetchOnWindowFocus: process.env.NODE_ENV === 'production', }, mutations: { retry: false, }, }, });

Query Key Factory

// lib/queryKeys.ts - Organized query keys export const queryKeys = { users: { all: ['users'] as const, lists: () => [...queryKeys.users.all, 'list'] as const, list: (filters: UserFilters) => [...queryKeys.users.lists(), filters] as const, details: () => [...queryKeys.users.all, 'detail'] as const, detail: (id: string) => [...queryKeys.users.details(), id] as const, }, posts: { all: ['posts'] as const, byUser: (userId: string) => [...queryKeys.posts.all, 'user', userId] as const, }, } as const;

// Usage const { data } = useQuery({ queryKey: queryKeys.users.detail(userId), queryFn: () => fetchUser(userId), });

// Invalidate all user queries queryClient.invalidateQueries({ queryKey: queryKeys.users.all });

Error Handling

// hooks/useApiQuery.ts import { UseQueryOptions, useQuery } from '@tanstack/react-query';

interface ApiError { status: number; message: string; }

export function useApiQuery<TData>( options: UseQueryOptions<TData, ApiError> ) { return useQuery({ ...options, throwOnError: (error) => { // Only throw for critical errors that should trigger error boundary return error.status >= 500; }, }); }

// With error boundary function UserProfile({ userId }: { userId: string }) { const { data, error, isLoading } = useApiQuery({ queryKey: queryKeys.users.detail(userId), queryFn: () => fetchUser(userId), });

if (isLoading) return <Skeleton />;

if (error) { if (error.status === 404) return <NotFound />; return <ErrorMessage message={error.message} />; }

return <UserCard user={data} />; }

Prefetching & SSR

// Next.js App Router example // app/users/page.tsx import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';

export default async function UsersPage() { const queryClient = new QueryClient();

await queryClient.prefetchQuery({ queryKey: queryKeys.users.lists(), queryFn: fetchUsers, });

return ( <HydrationBoundary state={dehydrate(queryClient)}> <UserList /> </HydrationBoundary> ); }

// Prefetch on hover function UserLink({ userId }: { userId: string }) { const queryClient = useQueryClient();

const prefetchUser = () => { queryClient.prefetchQuery({ queryKey: queryKeys.users.detail(userId), queryFn: () => fetchUser(userId), staleTime: 1000 * 60, }); };

return ( <Link href={/users/${userId}} onMouseEnter={prefetchUser}> View User </Link> ); }

Testing

// test-utils.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false }, }, });

export function renderWithQuery(ui: React.ReactElement) { const queryClient = createTestQueryClient(); return { ...render( <QueryClientProvider client={queryClient}> {ui} </QueryClientProvider> ), queryClient, }; }

// Test example test('displays user data', async () => { server.use( http.get('/api/users/:id', () => { return HttpResponse.json({ id: '1', name: 'John' }); }) );

renderWithQuery(<UserProfile userId="1" />);

expect(await screen.findByText('John')).toBeInTheDocument(); });

Monitoring Metrics

Metric Target

Cache hit ratio

80%

Average query time < 200ms

Stale data served < 5%

Background refetch errors < 1%

Checklist

  • Query key factory pattern

  • Global error handling in QueryCache

  • Retry logic for transient failures

  • staleTime and gcTime configured

  • Optimistic updates for mutations

  • Prefetching for navigation

  • SSR hydration setup

  • Test utilities with fresh QueryClient

  • DevTools in development only

  • Error boundaries for critical failures

OpenAPI Integration

TanStack Query works seamlessly with generated OpenAPI clients for type-safe data fetching.

With Generated Types

import type { paths, components } from '@/types/api'; import { useQuery, useMutation } from '@tanstack/react-query';

type User = components['schemas']['User']; type CreateUserInput = components['schemas']['CreateUserInput'];

// Type-safe query function useUser(id: string) { return useQuery<User>({ queryKey: ['users', id], queryFn: async () => { const res = await fetch(/api/users/${id}); return res.json(); }, }); }

// Type-safe mutation function useCreateUser() { return useMutation<User, Error, CreateUserInput>({ mutationFn: async (input) => { const res = await fetch('/api/users', { method: 'POST', body: JSON.stringify(input), }); return res.json(); }, }); }

With openapi-fetch

import createClient from 'openapi-fetch'; import type { paths } from '@/types/api';

const api = createClient<paths>({ baseUrl: '/api' });

function useUsers() { return useQuery({ queryKey: ['users'], queryFn: async () => { const { data, error } = await api.GET('/users'); if (error) throw error; return data; }, }); }

Related Skills

Skill Purpose

OpenAPI Codegen Generate types from spec

HTTP Clients Axios/fetch integration

React API Alternative patterns (SWR)

Reference Documentation

  • Query Keys

  • Caching Strategies

  • OpenAPI Types

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

cron-scheduling

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

token-optimization

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

webrtc

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review