$npx -y skills add jgamaraalv/ts-dev-kit --skill tanstack-queryTanStack Query v5 (React Query) reference for data fetching, caching, and server state management in React. Use when: (1) writing useQuery, useMutation, or useInfiniteQuery hooks, (2) setting up QueryClient and queryOptions, (3) implementing optimistic updates or cache invalidati
| 1 | # TanStack Query v5 (React) |
| 2 | |
| 3 | <quick_reference> |
| 4 | |
| 5 | ## Setup |
| 6 | |
| 7 | ```tsx |
| 8 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' |
| 9 | |
| 10 | const queryClient = new QueryClient({ |
| 11 | defaultOptions: { |
| 12 | queries: { |
| 13 | staleTime: 60 * 1000, // 1 min (default is 0) |
| 14 | gcTime: 5 * 60 * 1000, // 5 min (default) |
| 15 | retry: 3, // 3 retries with exponential backoff (default) |
| 16 | refetchOnWindowFocus: true, // default |
| 17 | }, |
| 18 | }, |
| 19 | }) |
| 20 | |
| 21 | function App() { |
| 22 | return ( |
| 23 | <QueryClientProvider client={queryClient}> |
| 24 | <YourApp /> |
| 25 | </QueryClientProvider> |
| 26 | ) |
| 27 | } |
| 28 | ``` |
| 29 | |
| 30 | ## Important Defaults |
| 31 | |
| 32 | | Default | Value | Notes | |
| 33 | |---------|-------|-------| |
| 34 | | `staleTime` | `0` | Cached data is immediately stale; triggers background refetch on mount/focus/reconnect | |
| 35 | | `gcTime` | `5 min` | Inactive queries garbage collected after 5 minutes | |
| 36 | | `retry` | `3` (queries) / `0` (mutations) | Queries retry 3x with exponential backoff; mutations do NOT retry | |
| 37 | | `refetchOnWindowFocus` | `true` | Stale queries refetch when tab regains focus | |
| 38 | | `refetchOnReconnect` | `true` | Stale queries refetch when network reconnects | |
| 39 | | `refetchOnMount` | `true` | Stale queries refetch when new instance mounts | |
| 40 | | `structuralSharing` | `true` | Preserves referential identity if data is structurally equal | |
| 41 | |
| 42 | **Key recommendation:** Set `staleTime` above 0 to control refetch frequency rather than disabling individual refetch triggers. |
| 43 | |
| 44 | </quick_reference> |
| 45 | |
| 46 | <examples> |
| 47 | |
| 48 | ## queryOptions — co-locate key + fn |
| 49 | |
| 50 | Always use `queryOptions` to define query configurations. It enables type inference across `useQuery`, `prefetchQuery`, `getQueryData`, and `setQueryData`. |
| 51 | |
| 52 | ```tsx |
| 53 | import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query' |
| 54 | |
| 55 | export function todosOptions(filters: TodoFilters) { |
| 56 | return queryOptions({ |
| 57 | queryKey: ['todos', filters], |
| 58 | queryFn: () => fetchTodos(filters), |
| 59 | staleTime: 5 * 1000, |
| 60 | }) |
| 61 | } |
| 62 | |
| 63 | // Works everywhere with full type inference: |
| 64 | useQuery(todosOptions({ status: 'done' })) |
| 65 | useSuspenseQuery(todosOptions({ status: 'done' })) |
| 66 | queryClient.prefetchQuery(todosOptions({ status: 'done' })) |
| 67 | queryClient.setQueryData(todosOptions({ status: 'done' }).queryKey, newData) |
| 68 | const cached = queryClient.getQueryData(todosOptions({ status: 'done' }).queryKey) |
| 69 | // ^? TodoItem[] | undefined |
| 70 | ``` |
| 71 | |
| 72 | For infinite queries, use `infiniteQueryOptions` (same pattern, adds `initialPageParam` and `getNextPageParam`). |
| 73 | |
| 74 | ## useQuery |
| 75 | |
| 76 | ```tsx |
| 77 | const { |
| 78 | data, // TData | undefined |
| 79 | error, // TError | null |
| 80 | status, // 'pending' | 'error' | 'success' |
| 81 | isPending, // no cached data yet |
| 82 | isError, |
| 83 | isSuccess, |
| 84 | isFetching, // queryFn is running (including background refetch) |
| 85 | isLoading, // isPending && isFetching (first load only) |
| 86 | isPlaceholderData, // showing placeholder, not real data |
| 87 | isStale, |
| 88 | refetch, |
| 89 | fetchStatus, // 'fetching' | 'paused' | 'idle' |
| 90 | } = useQuery({ |
| 91 | queryKey: ['todos', userId], // unique cache key (Array) |
| 92 | queryFn: () => fetchTodos(userId), |
| 93 | enabled: !!userId, // disable until userId exists |
| 94 | staleTime: 60_000, |
| 95 | select: (data) => data.filter(t => !t.done), // transform/filter |
| 96 | placeholderData: keepPreviousData, // smooth pagination |
| 97 | }) |
| 98 | ``` |
| 99 | |
| 100 | **Query states:** `status` tells you "do we have data?"; `fetchStatus` tells you "is the queryFn running?". They are orthogonal — a query can be `pending` + `paused` (no data, no network). |
| 101 | |
| 102 | ## Query Keys |
| 103 | |
| 104 | Keys must be Arrays. They are hashed deterministically. |
| 105 | |
| 106 | ```tsx |
| 107 | // Object key order does NOT matter — these are equivalent: |
| 108 | useQuery({ queryKey: ['todos', { status, page }] }) |
| 109 | useQuery({ queryKey: ['todos', { page, status }] }) |
| 110 | |
| 111 | // Array item order DOES matter — these are different: |
| 112 | useQuery({ queryKey: ['todos', status, page] }) |
| 113 | useQuery({ queryKey: ['todos', page, status] }) |
| 114 | ``` |
| 115 | |
| 116 | **Rule:** If your queryFn depends on a variable, include it in the queryKey. The key acts as a dependency array. |
| 117 | |
| 118 | ## useMutation |
| 119 | |
| 120 | ```tsx |
| 121 | const queryClient = useQueryClient() |
| 122 | |
| 123 | const mutation = useMutation({ |
| 124 | mutationFn: (newTodo: CreateTodoInput) => api.post('/todos', newTodo), |
| 125 | onSuccess: (data, variables) => { |
| 126 | // Invalidate related queries to trigger refetch |
| 127 | queryClient.invalidateQueries({ queryKey: ['todos'] }) |
| 128 | // Or update cache directly with response data |
| 129 | queryClient.setQueryData(['todos', data.id], data) |
| 130 | }, |
| 131 | onError: (error, variables, on |