Claude
Skills
Sign in
Back

tanstack-query

Included with Lifetime
$97 forever

This skill provides comprehensive knowledge for TanStack Query v5 (React Query) server state management in React applications. It should be used when setting up data fetching with useQuery, implementing mutations with useMutation, configuring QueryClient, managing caching strategies, migrating from v4 to v5, implementing optimistic updates, using infinite queries, or encountering query/mutation errors. Use when: initializing TanStack Query in React projects, configuring QueryClient settings, creating custom query hooks, implementing mutations with error handling, setting up optimistic updates, using useInfiniteQuery for pagination, migrating from React Query v4 to v5, debugging stale data issues, fixing caching problems, resolving v5 breaking changes, implementing suspense queries, or setting up query devtools. Keywords: TanStack Query, React Query, useQuery, useMutation, useInfiniteQuery, useSuspenseQuery, QueryClient, QueryClientProvider, data fetching, server state, caching, staleTime, gcTime, query invalidation, prefetching, optimistic updates, mutations, query keys, query functions, error boundaries, suspense, React Query DevTools, v5 migration, v4 to v5, request waterfalls, background refetching, cacheTime renamed, loading status renamed, pending status, initialPageParam required, keepPreviousData removed, placeholderData, query callbacks removed, onSuccess removed, onError removed, object syntax required

Web Dev

What this skill does


# TanStack Query (React Query) v5

**Status**: Production Ready ✅
**Last Updated**: 2025-10-22
**Dependencies**: React 18.0+, TypeScript 4.7+ (recommended)
**Latest Versions**: @tanstack/[email protected], @tanstack/[email protected]

---

## Quick Start (5 Minutes)

### 1. Install Dependencies

```bash
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest
```

**Why this matters:**
- TanStack Query v5 requires React 18+ (uses useSyncExternalStore)
- DevTools are essential for debugging queries and mutations
- v5 has breaking changes from v4 - use latest for all fixes

### 2. Set Up QueryClient Provider

```tsx
// src/main.tsx or src/index.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import App from './App'

// Create a client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 60, // 1 hour (formerly cacheTime)
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
})

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </StrictMode>
)
```

**CRITICAL:**
- Wrap entire app with `QueryClientProvider`
- Configure `staleTime` to avoid excessive refetches (default is 0)
- Use `gcTime` (not `cacheTime` - renamed in v5)
- DevTools should be inside provider

### 3. Create First Query

```tsx
// src/hooks/useTodos.ts
import { useQuery } from '@tanstack/react-query'

type Todo = {
  id: number
  title: string
  completed: boolean
}

async function fetchTodos(): Promise<Todo[]> {
  const response = await fetch('/api/todos')
  if (!response.ok) {
    throw new Error('Failed to fetch todos')
  }
  return response.json()
}

export function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
}

// Usage in component:
function TodoList() {
  const { data, isPending, isError, error } = useTodos()

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}
```

**CRITICAL:**
- v5 requires object syntax: `useQuery({ queryKey, queryFn })`
- Use `isPending` (not `isLoading` - that now means "pending AND fetching")
- Always throw errors in queryFn for proper error handling
- QueryKey should be array for consistent cache keys

### 4. Create First Mutation

```tsx
// src/hooks/useAddTodo.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'

type NewTodo = {
  title: string
}

async function addTodo(newTodo: NewTodo) {
  const response = await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newTodo),
  })
  if (!response.ok) throw new Error('Failed to add todo')
  return response.json()
}

export function useAddTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: addTodo,
    onSuccess: () => {
      // Invalidate and refetch todos
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

// Usage in component:
function AddTodoForm() {
  const { mutate, isPending } = useAddTodo()

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    mutate({ title: formData.get('title') as string })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Adding...' : 'Add Todo'}
      </button>
    </form>
  )
}
```

**Why this works:**
- Mutations use callbacks (`onSuccess`, `onError`, `onSettled`) - queries don't
- `invalidateQueries` triggers background refetch
- Mutations don't cache by default (correct behavior)

---

## The 7-Step Setup Process

### Step 1: Install Dependencies

```bash
# Core library (required)
npm install @tanstack/react-query

# DevTools (highly recommended for development)
npm install -D @tanstack/react-query-devtools

# Optional: ESLint plugin for best practices
npm install -D @tanstack/eslint-plugin-query
```

**Package roles:**
- `@tanstack/react-query` - Core React hooks and QueryClient
- `@tanstack/react-query-devtools` - Visual debugger (dev only, tree-shakeable)
- `@tanstack/eslint-plugin-query` - Catches common mistakes

**Version requirements:**
- React 18.0 or higher (uses `useSyncExternalStore`)
- TypeScript 4.7+ for best type inference (optional but recommended)

### Step 2: Configure QueryClient

```tsx
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // How long data is considered fresh (won't refetch during this time)
      staleTime: 1000 * 60 * 5, // 5 minutes

      // How long inactive data stays in cache before garbage collection
      gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime)

      // Retry failed requests (0 on server, 3 on client by default)
      retry: (failureCount, error) => {
        if (error instanceof Response && error.status === 404) return false
        return failureCount < 3
      },

      // Refetch on window focus (can be annoying during dev)
      refetchOnWindowFocus: false,

      // Refetch on network reconnect
      refetchOnReconnect: true,

      // Refetch on component mount if data is stale
      refetchOnMount: true,
    },
    mutations: {
      // Retry mutations on failure (usually don't want this)
      retry: 0,
    },
  },
})
```

**Key configuration decisions:**

**staleTime vs gcTime:**
- `staleTime`: How long until data is considered "stale" and might refetch
  - `0` (default): Data is immediately stale, refetches on mount/focus
  - `1000 * 60 * 5`: Data fresh for 5 min, no refetch during this time
  - `Infinity`: Data never stale, manual invalidation only
- `gcTime`: How long unused data stays in cache
  - `1000 * 60 * 5` (default): 5 minutes
  - `Infinity`: Never garbage collect (memory leak risk)

**When to refetch:**
- `refetchOnWindowFocus: true` - Good for frequently changing data (stock prices)
- `refetchOnWindowFocus: false` - Good for stable data or during development
- `refetchOnMount: true` - Ensures fresh data when component mounts
- `refetchOnReconnect: true` - Refetch after network reconnect

### Step 3: Wrap App with Provider

```tsx
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './lib/query-client'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools
        initialIsOpen={false}
        buttonPosition="bottom-right"
      />
    </QueryClientProvider>
  </StrictMode>
)
```

**Provider placement:**
- Must wrap all components that use TanStack Query hooks
- DevTools must be inside provider
- Only one QueryClient instance for entire app

**DevTools configuration:**
- `initialIsOpen={false}` - Collapsed by default
- `buttonPosition="bottom-right"` - Where to show toggle button
- Automatically removed in production builds (tree-shaken)

### Step 4: Create Custom Query Hooks

**Pattern: Reusable Query Hooks**

```tsx
// src/api/todos.ts - API functions
export type Todo = {
  id: number
  title: string
  completed: boolean
}

export async function fetchTodos(): Promise<Todo[]> {
  const response = await fetch('/api/todos')
  if
Files: 4
Size: 136.8 KB
Complexity: 49/100
Category: Web Dev

Related in Web Dev