Claude
Skills
Sign in
Back

tanstack-query

Included with Lifetime
$97 forever

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.

General

What this skill does


# 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,
     refetchI

Related in General