Skip to main content

tanstack-query

Comprehensive TanStack Query v5 patterns for async state management. Covers breaking changes, query key factories, data transformation, mutations, optimistic updates, authentication, testing with MSW, and anti-patterns. Use for all server state management, data fetching, and cache invalidation tasks.

Source facts

Repository
MadAppGang/claude-code
Last source activity
January 31, 2026 at 03:08
Detected SKILL.md language
English
Stars
283
Forks
26

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
tanstack-query
description
Comprehensive TanStack Query v5 patterns for async state management. Covers breaking changes, query key factories, data transformation, mutations, optimistic updates, authentication, testing with MSW, and anti-patterns. Use for all server state management, data fetching, and cache invalidation tasks.
# TanStack Query v5 - Complete Guide **TanStack Query v5** (October 2023) is the async state manager for this project. It requires React 18+, features first-class Suspense support, improved TypeScript inference, and a 20% smaller bundle. This section covers production-ready patterns based on official documentation and community best practices. ### Breaking Changes in v5 **Key updates you need to know:** 1. **Single Object Signature**: All hooks now accept one configuration object: ```typescript // ✅ v5 - single object useQuery({ queryKey, queryFn, ...options }) // ❌ v4 - multiple overloads (deprecated) useQuery(queryKey, queryFn, options) ``` 2. **Renamed Options**: - `cacheTime` → `gcTime` (garbage collection time) - `keepPreviousData` → `placeholderData: keepPreviousData` - `isLoading` now means `isPending && isFetching` 3. **Callbacks Removed from useQuery**: - `onSuccess`, `onError`, `onSettled` removed from `useQuery` - Use global QueryCache callbacks instead - Prevents duplicate executions 4. **Infinite Queries Require initialPageParam**: - No default value provided - Must explicitly set `initialPageParam` (e.g., `0` or `null`) 5. **First-Class Suspense**: - New dedicated hooks: `useSuspenseQuery`, `useSuspenseInfiniteQuery` - No experimental flag needed - Data is never undefined at type level **Migration**: Use the official codemod for automatic migration: `npx @tanstack/query-codemods v5/replace-import-specifier` ### Smart Defaults Query v5 ships with production-ready defaults: ```typescript { staleTime: 0, // Data instantly stale (refetch on mount) gcTime: 5 * 60_000, // Keep unused cache for 5 minutes retry: 3, // 3 retries with exponential backoff refetchOnWindowFocus: true,// Refetch when user returns to tab refetchOnReconnect: true, // Refetch when network reconnects } ``` **Philosophy**: React Query is an **async state manager, not a data fetcher**. You provide the Promise; Query manages caching, background updates, and synchronization. ### Client Setup ```typescript // src/app/providers.tsx import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query' import { toast } from './toast' // Your notification system const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 0, // Adjust per-query gcTime: 5 * 60_000, // 5 minutes (v5: formerly cacheTime) retry: (failureCount, error) => { // Don't retry on 401 (authentication errors) if (error?.response?.status === 401) return false return failureCount < 3 }, }, }, queryCache: new QueryCache({ onError: (error, query) => { // Only show toast for background errors (when data exists) if (query.state.data !== undefined) { toast.error(`Something went wrong: ${error.message}`) } }, }), }) export function AppProviders({ children }: { children: React.ReactNode }) { return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ) } ``` **DevTools Setup** (auto-excluded in production): ```typescript import { ReactQueryDevtools } from '@tanstack/react-query-devtools' <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ``` ### Server-Side Rendering Configuration When using TanStack Query with SSR (Next.js, Remix, TanStack Start), configure server-specific defaults: ```typescript // Server-side QueryClient configuration export function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // Server: Don't retry on server (fail fast) retry: typeof window === 'undefined' ? 0 : 3, // Server: Data is always fresh when rendered staleTime: 60_000, // 1 minute }, }, }) } ``` **Server vs Client Defaults:** | Option | Client Default | Server Recommended | Why | |--------|---------------|-------------------|-----| | `retry` | 3 | 0 | Server should fail fast, not retry loops | | `staleTime` | 0 | 60_000+ | Server-rendered data is fresh | | `gcTime` | 5 min | Infinity | No garbage collection needed on server | | `refetchOnWindowFocus` | true | false | No window on server | | `refetchOnReconnect` | true | false | No reconnect on server | **Important:** In SPA-only apps (TanStack Router + Vite), you don't need these server defaults. They're only relevant for SSR frameworks. ### Streaming SSR (Experimental) For Next.js App Router, `@tanstack/react-query-next-experimental` enables streaming: ```bash pnpm add @tanstack/react-query-next-experimental ``` **Setup:** ```typescript // app/providers.tsx 'use client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 }, }, }) } let browserQueryClient: QueryClient | undefined function getQueryClient() { if (typeof window === 'undefined') { return makeQueryClient() // Server: always new } return (browserQueryClient ??= makeQueryClient()) // Browser: singleton } export function Providers({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration> </QueryClientProvider> ) } ``` **Usage in Client Components:** ```typescript 'use client' import { useSuspenseQuery } from '@tanstack/react-query' export function UserProfile({ userId }: { userId: string }) { // No prefetch needed! Data streams from server const { data: user } = useSuspenseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }) return <div>{user.name}</div> } ``` **Benefits:** - Skip manual prefetching in Server Components - Data streams to client as it resolves - Suspense boundaries show loading states naturally **Limitations:** - Next.js App Router only (experimental) - Not for TanStack Router SPAs (use route loaders instead) ### Server Components Integration **When you have React Server Components (RSC)**, how does TanStack Query fit? #### The Mental Model Think of Server Components as **another framework loader** (like route loaders): | Feature | Server Components | TanStack Query | |---------|-------------------|----------------| | Initial data fetch | Yes (server) | Yes (client prefetch) | | Client mutations | No | Yes | | Background refetch | No | Yes | | Optimistic updates | No | Yes | | Real-time updates | No | Yes | | Cache management | No | Yes | #### When TanStack Query is Still Valuable Even in RSC-heavy apps, Query remains essential for: 1. **Client-Side Mutations** ```typescript // Server Component fetches, Client handles mutations export default async function PostPage({ params }) { const post = await fetchPost(params.id) // Server fetch return <PostWithComments post={post} /> // Client mutations } 'use client' function PostWithComments({ post }) { const addComment = useMutation({ ... }) // Still need Query! // ... } ``` 2. **Background Refetching After Initial Load** ```typescript // Initial: Server Component renders with fresh data // After: Query keeps data fresh on client ``` 3. **Optimistic Updates** ```typescript // Can't do optimistic updates with Server Components alone const likeMutation = useMutation({ mutationFn: likePost, onMutate: async () => { // Optimistic update - only possible with Query }, }) ``` 4. **Real-Time Updates** ```typescript // WebSocket data, polling, etc. - client-only useQuery({ queryKey: ['notifications'], queryFn: fetchNotifications, refetchInterval: 30_000, // Real-time polling }) ``` #### Recommended Pattern ```typescript // Server Component: Initial fetch export default async function DashboardPage() { const initialData = await fetchDashboard() return ( <HydrationBoundary state={dehydrate(queryClient)}> <DashboardClient initialData={initialData} /> </HydrationBoundary> ) } // Client Component: Mutations + real-time 'use client' function DashboardClient({ initialData }) { // Query hydrates from server data, then manages client state const { data } = useQuery({ queryKey: ['dashboard'], queryFn: fetchDashboard, initialData, }) const updateWidget = useMutation({ ... }) // ... } ``` #### SPA Recommendation **For SPA-only apps (TanStack Router + Vite)**: Server Components don't apply. Use TanStack Query as your primary data layer with route loaders for prefetching. ### Query + React 19 Actions When using React 19 Actions alongside Query, keep responsibilities clear: #### Complementary Usage ```typescript // Query: Fetching and caching const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, }) // Action: Form submission with built-in validation async function createPostAction(formData: FormData) { 'use server' const result = await createPost(formData) return result } // After action succeeds, invalidate Query cache const [state, formAction] = useActionState(async (prev, formData) => { const result = await createPostAction(formData) if (result.success) { queryClient.invalidateQueries({ queryKey: ['posts'] }) } return result }, { success: false }) ``` #### Decision Matrix | Use Case | Recommendation | |----------|----------------| | Data fetching | Query (`useQuery`) | | List caching | Query | | Form submission | Action (`useActionState`) + Query invalidation | | Button click mutation | Query (`useMutation`) | | Optimistic update with rollback | Query (`useMutation`) | | Server-side validation | Action | | Complex multi-step mutations | Query (`useMutation`) | #### Rule of Thumb - **Actions** for form submissions with server-side validation - **Query** for everything else (fetching, caching, complex mutations) - **Both** when forms need to update Query cache after success ### Architecture: Feature-Based Colocation **Recommended pattern**: Group queries with related features, not by file type. ``` src/features/ ├── Todos/ │ ├── index.tsx # Feature entry point │ ├── queries.ts # All React Query logic (keys, functions, hooks) │ ├── types.ts # TypeScript types │ └── components/ # Feature-specific components ``` **Export only custom hooks** from query files. Keep query functions and keys private: ```typescript // features/todos/queries.ts // 1. Query Key Factory (hierarchical structure) const todoKeys = { all: ['todos'] as const, lists: () => [...todoKeys.all, 'list'] as const, list: (filters: string) => [...todoKeys.lists(), { filters }] as const, details: () => [...todoKeys.all, 'detail'] as const, detail: (id: number) => [...todoKeys.details(), id] as const, } // 2. Query Function (private) const fetchTodos = async (filters: string): Promise<Todo[]> => { const response = await axios.get('/api/todos', { params: { filters } }) return response.data } // 3. Custom Hook (public API) export const useTodosQuery = (filters: string) => { return useQuery({ queryKey: todoKeys.list(filters), queryFn: () => fetchTodos(filters), staleTime: 30_000, // Fresh for 30 seconds }) } ``` **Benefits**: - Prevents key/function mismatches - Clean public API - Encapsulation and maintainability - Easy to locate all query logic for a feature ### Query Key Factories (Essential) **Structure keys hierarchically** from generic to specific: ```typescript // ✅ Correct hierarchy ['todos'] // Invalidates everything ['todos', 'list'] // Invalidates all lists ['todos', 'list', { filters }] // Invalidates specific list ['todos', 'detail', 1] // Invalidates specific detail
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub