Back to Skills

Tanstack Query

TanStack Query v5 (React Query) server state management. Use for data fetching, caching, mutations, or encountering v4 migration, stale data, invalidation errors.

react
By secondsky
17928Updated 1 day agoTypeScriptMIT

Skill Content

# TanStack Query (React Query) v5

**Status**: Production Ready ✅
**Last Updated**: 2025-12-09
**Dependencies**: React 18.0+ (18.3+ recommended), TypeScript 4.9+ (5.x preferred)
**Latest Versions**: @tanstack/react-query@5.90.12, @tanstack/react-query-devtools@5.91.1, @tanstack/eslint-plugin-query@5.91.2

---

## Quick Start (5 Minutes)

### 1. Install Dependencies

```bash
# choose your package manager
pnpm add @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# or
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# or
bun add @tanstack/react-query@latest @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

**Know the defaults (v5):**
- `staleTime: 0` → data is immediately stale, so refetches on mount/focus unless you raise it
- `gcTime: 5 * 60 * 1000` → inactive data is garbage-collected after 5 minutes
- `retry: 3` in browsers, `retry: 0` on the server
- `refetchOnWindowFocus: true` and `refetchOnReconnect: true`
- `networkMode: 'online'` (requests pause while offline). Switch to `'always'` for SSR/prefetch where you don't want cancellation. citeturn1search0turn1search1

### 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)
pnpm add @tanstack/react-query

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

# Optional: ESLint plugin for best practices
pnpm add -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 5.2+ 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)

### Advanced Setup (Steps 4-7)

**For detailed patterns**: Load `references/advanced-setup.md` when implementing custom query hooks, mutations with optimistic updates, DevTools configuration, or error boundaries.

**Quick summaries:**

**Step 4: Custom Query Hooks** - Use `queryOptions` factory for reusable patterns. Create custom hooks that encapsulate API calls.

**Step 5: Mutations** - Use `useMutation` with `onSuccess` to invalidate queries. For instant UI feedback, implement optimistic updates with `onMutate`/`onError`/`onSettled` pattern.

**Step 6: DevTools** - Already included in Step 3. Advanced options for customization available in reference.

**Step 7: Error Boundaries** - Use `QueryErrorResetBoundary` with React Error Boundary. Configure `throwOnError` option for global vs local error handling.

---

## Critical Rules

### Always Do

✅ **Use object syntax for all hooks**
```tsx
// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })
```

✅ **Use array query keys**
```tsx
queryKey: ['todos']              // List
queryKey: ['todos', id]          // Detail
queryKey: ['todos', { filter }]  // Filtered
```

✅ **Configure staleTime appropriately**
```tsx
staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches
```

✅ **Use isPending for initial loading state**
```tsx
if (isPending) return <Loading />
// isPending = no data yet AND fetching
```

✅ **Throw errors in queryFn**
```tsx
if (!response.ok) throw new Error('Failed')
```

✅ **Invalidate queries after mutations**
```tsx
onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}
```

✅ **Use queryOptions factory for reusable patterns**
```tsx
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)
```

✅ **Use gcTime (not cacheTime)**
```tsx
gcTime: 1000 * 60 * 60 // 1 hour
```

✅ **Know your status flags**
```tsx
isPending      // no data yet, fetch in flight
isFetching     // any fetch in flight (including refetch)
isRefetching   // refetch specifically (data already cached)
isLoadingError // initial load failed
isPaused       // networkMode paused (e.g., offline)
isFetchingNextPage // useInfiniteQuery loading more
```

### Never Do

❌ **Never use v4 array/function syntax**
```tsx
// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌

// v5 (correct):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅
```

❌ **Never use query callbacks (onSuccess, onError, onSettled in queries)**
```tsx
// v5 removed these from queries:
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {}, // ❌ Removed in v5
})

// Use useEffect instead:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // Do something
  }
}, [data])

// Or use mutation callbacks (still supported):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => {}, // ✅ Still works for mutations
})
```

❌ **Never use deprecated options**
```tsx
// Deprecated in v5:
cacheTime: 1000 // ❌ Use gcTime instead
isLoading: true // ❌ Meaning changed, use isPending
keepPreviousData: true // ❌ Use placeholderData instead
onSuccess: () => {} // ❌ Removed from queries
useErrorBoundary: true // ❌ Use throwOnError instead
```

❌ **Never assume isLoading means "no data yet"**
```tsx
// v5 changed this:
isLoading = isPending && isFetching // ❌ Now means "pending AND fetching"
isPending = no data yet // ✅ Use this for initial load
```

❌ **Never forget initialPageParam for infinite queries**
```tsx
// v5 requires this:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ Required in v5
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
```

❌ **Never use enabled with useSuspenseQuery**
```tsx
// Not allowed:
useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ Not available with suspense
})

// Use conditional rendering instead:
{id && <TodoComponent id={id} />}
```

---

## Error Prevention

This skill prevents **8+ documented v5 migration issues**. The most critical errors include:
- Object syntax required (v4 function overloads removed)
- Query callbacks removed (onSuccess/onError/onSettled)
- `isPending` vs `isLoading` status changes
- `cacheTime` renamed to `gcTime`
- `initialPageParam` required for infinite queries
- `keepPreviousData` replaced with `placeholderData`

**For complete error catalog with before/after examples**: Load `references/top-errors.md` when encountering errors or debugging v5 migration issues.

---

## Project Configuration

**Essential configuration files**: package.json, tsconfig.json, .eslintrc.cjs

**Key requirements**:
- React 18.3.1+ (uses useSyncExternalStore)
- TypeScript strict mode recommended
- ESLint plugin catches v4→v5 migration errors

**For complete configuration templates**: Load `references/configuration-files.md` when setting up new projects or troubleshooting build/type errors.

---

## Common Patterns

### Pattern 1: Dependent Queries

```tsx
// Fetch user, then fetch user's posts
function UserPosts({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  const { data: posts } = useQuery({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!user, // Only fetch posts after user is loaded
  })

  if (!user) return <div>Loading user...</div>
  if (!posts) return <div>Loading posts...</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}
```

**When to use**: Query B depends on data from Query A

### Pattern 2: Parallel Queries with useQueries

```tsx
// Fetch multiple todos in parallel
function TodoDetails({ ids }: { ids: number[] }) {
  const results = useQueries({
    queries: ids.map(id => ({
      queryKey: ['todos', id],
      queryFn: () => fetchTodo(id),
    })),
  })

  const isLoading = results.some(result => result.isPending)
  const isError = results.some(result => result.isError)

  if (isLoading) return <div>Loading...</div>
  if (isError) return <div>Error loading todos</div>

  return (
    <ul>
      {results.map((result, i) => (
        <li key={ids[i]}>{result.data?.title}</li>
      ))}
    </ul>
  )
}
```

**When to use**: Fetch multiple independent queries in parallel

### Pattern 3: Prefetching

```tsx
import { useQueryClient } from '@tanstack/react-query'
import { todosQueryOptions } from './hooks/useTodos'

function TodoListWithPrefetch() {
  const queryClient = useQueryClient()
  const { data: todos } = useTodos()

  const prefetchTodo = (id: number) => {
    queryClient.prefetchQuery({
      queryKey: ['todos', id],
      queryFn: () => fetchTodo(id),
      staleTime: 1000 * 60 * 5, // 5 minutes
    })
  }

  return (
    <ul>
      {todos?.map(todo => (
        <li
          key={todo.id}
          onMouseEnter={() => prefetchTodo(todo.id)}
        >
          <Link to={`/todos/${todo.id}`}>{todo.title}</Link>
        </li>
      ))}
    </ul>
  )
}
```

**When to use**: Preload data before user navigates (on hover, on mount)

### Pattern 4: Infinite Scroll

```tsx
import { useInfiniteQuery } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'

type Page = {
  data: Todo[]
  nextCursor: number | null
}

async function fetchTodosPage({ pageParam }: { pageParam: number }): Promise<Page> {
  const response = await fetch(`/api/todos?cursor=${pageParam}&limit=20`)
  return response.json()
}

function InfiniteTodoList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['todos', 'infinite'],
    queryFn: fetchTodosPage,
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  })

  const loadMoreRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage()
        }
      },
      { threshold: 0.1 }
    )

    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current)
    }

    return () => observer.disconnect()
  }, [fetchNextPage, hasNextPage])

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.data.map(todo => (
            <div key={todo.id}>{todo.title}</div>
          ))}
        </div>
      ))}

      <div ref={loadMoreRef}>
        {isFetchingNextPage && <div>Loading more...</div>}
      </div>
    </div>
  )
}
```

**When to use**: Paginated lists with infinite scroll

### Pattern 5: Query Cancellation

```tsx
function SearchTodos() {
  const [search, setSearch] = useState('')

  const { data } = useQuery({
    queryKey: ['todos', 'search', search],
    queryFn: async ({ signal }) => {
      const response = await fetch(`/api/todos?q=${search}`, { signal })
      return response.json()
    },
    enabled: search.length > 2, // Only search if 3+ characters
  })

  return (
    <div>
      <input
        value={search}
        onChange={e => setSearch(e.target.value)}
        placeholder="Search todos..."
      />
      {data && (
        <ul>
          {data.map(todo => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      )}
    </div>
  )
}
```

**How it works:**
- When queryKey changes, previous query is automatically cancelled
- Pass `signal` to fetch for proper cleanup
- Browser aborts pending fetch requests

### Pattern 6: Background Fetch Indicators

```tsx
const { data, isFetching, isRefetching } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 1000 * 60 * 5,
})

return (
  <div>
    {isFetching && <Spinner label={isRefetching ? 'Refreshing…' : 'Loading…'} />}
    <TodoList data={data} />
  </div>
)
```

**Why:** `isFetching` stays true during background refetches so you can show a subtle "Refreshing" badge without losing cached data.

---

## Using Bundled Resources

### Templates (templates/)

Complete, copy-ready code examples:

- `package.json` - Dependencies with exact versions
- `query-client-config.ts` - QueryClient setup with best practices
- `provider-setup.tsx` - App wrapper with QueryClientProvider
- `use-query-basic.tsx` - Basic useQuery hook pattern
- `use-mutation-basic.tsx` - Basic useMutation hook
- `use-mutation-optimistic.tsx` - Optimistic update pattern
- `use-infinite-query.tsx` - Infinite scroll pattern
- `custom-hooks-pattern.tsx` - Reusable query hooks with queryOptions
- `error-boundary.tsx` - Error boundary with query reset
- `devtools-setup.tsx` - DevTools configuration

**Example Usage:**
```bash
# Copy query client config
cp ~/.claude/skills/tanstack-query/templates/query-client-config.ts src/lib/

# Copy provider setup
cp ~/.claude/skills/tanstack-query/templates/provider-setup.tsx src/main.tsx

# Or run the bootstrap helper (installs deps + copies core files):
./scripts/example-script.sh . pnpm
```

### References (references/)

Deep-dive documentation loaded when needed:

- `advanced-setup.md` - Custom hooks, mutations, optimistic updates, DevTools, error boundaries
- `configuration-files.md` - Complete package.json, tsconfig.json, .eslintrc.cjs templates
- `v4-to-v5-migration.md` - Complete v4 → v5 migration guide
- `best-practices.md` - Request waterfalls, caching strategies, performance
- `common-patterns.md` - Reusable queries, optimistic updates, infinite scroll
- `official-guides-map.md` - When to open each official doc and what it covers
- `typescript-patterns.md` - Type safety, generics, type inference
- `testing.md` - Testing with MSW, React Testing Library
- `top-errors.md` - All 8+ errors with solutions

### Examples (examples/)

- `examples/README.md` - Index of top 10 scenarios with official links
- `basic.tsx` - Minimal list query
- `basic-graphql-request.tsx` - GraphQL client + select
- `optimistic-update.tsx` - onMutate snapshot/rollback
- `pagination.tsx` - paginated list with placeholderData
- `infinite-scroll.tsx` - useInfiniteQuery + IntersectionObserver
- `prefetching.tsx` - prefetch on hover before navigation
- `suspense.tsx` - useSuspenseQuery + boundary
- `default-query-function.ts` - global fetcher using queryKey
- `nextjs-app-router.tsx` - App Router prefetch + hydrate (`networkMode: 'always'`)
- `react-native.tsx` - offline-first with AsyncStorage persister

**When Claude should load these:**
- `advanced-setup.md` - When implementing custom query hooks, mutations, or error boundaries
- `configuration-files.md` - When setting up new projects or troubleshooting build/type errors
- `v4-to-v5-migration.md` - When migrating existing React Query v4 project
- `best-practices.md` - When optimizing performance or avoiding waterfalls
- `common-patterns.md` - When implementing specific features (infinite scroll, etc.)
- `typescript-patterns.md` - When dealing with TypeScript errors or type inference
- `testing.md` - When writing tests for components using TanStack Query
- `top-errors.md` - When encountering errors not covered in main SKILL.md

---

## Advanced Topics

### Data Transformations with select

```tsx
// Only subscribe to specific slice of data
function TodoCount() {
  const { data: count } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) => data.length, // Only re-render when count changes
  })

  return <div>Total todos: {count}</div>
}

// Transform data shape
function CompletedTodoTitles() {
  const { data: titles } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) =>
      data
        .filter(todo => todo.completed)
        .map(todo => todo.title),
  })

  return (
    <ul>
      {titles?.map((title, i) => (
        <li key={i}>{title}</li>
      ))}
    </ul>
  )
}
```

**Benefits:**
- Component only re-renders when selected data changes
- Reduces memory usage (less data stored in component state)
- Keeps query cache unchanged (other components get full data)

### Request Waterfalls (Anti-Pattern)

```tsx
// ❌ BAD: Sequential waterfalls
function BadUserProfile({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  const { data: posts } = useQuery({
    queryKey: ['posts', user?.id],
    queryFn: () => fetchPosts(user!.id),
    enabled: !!user,
  })

  const { data: comments } = useQuery({
    queryKey: ['comments', posts?.[0]?.id],
    queryFn: () => fetchComments(posts![0].id),
    enabled: !!posts && posts.length > 0,
  })

  // Each query waits for previous one = slow!
}

// ✅ GOOD: Fetch in parallel when possible
function GoodUserProfile({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  // Fetch posts AND comments in parallel
  const { data: posts } = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId), // Don't wait for user
  })

  const { data: comments } = useQuery({
    queryKey: ['comments', userId],
    queryFn: () => fetchUserComments(userId), // Don't wait for posts
  })

  // All 3 queries run in parallel = fast!
}
```

### Server State vs Client State

```tsx
// ❌ Don't use TanStack Query for client-only state
const { data: isModalOpen, setData: setIsModalOpen } = useMutation(...)

// ✅ Use useState for client state
const [isModalOpen, setIsModalOpen] = useState(false)

// ✅ Use TanStack Query for server state
const { data: todos } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})
```

**Rule of thumb:**
- Server state: Use TanStack Query (data from API)
- Client state: Use useState/useReducer (local UI state)
- Global client state: Use Zustand/Context (theme, auth token)

---

## Platform & Integration Notes

- **React Native**: Works the same as web. Use `@tanstack/query-async-storage-persister` to persist cache to AsyncStorage; avoid window-focus refetch logic. DevTools panel not available natively—use Flipper or expose logs.
- **GraphQL**: Pair with `graphql-request` or urql's bare client. Treat operations as plain async functions; co-locate fragments and use `select` to map edges/nodes to flat shapes.
- **SSR / Next.js / TanStack Start**: Use `dehydrate`/`HydrationBoundary` on the server and `QueryClientProvider` on the client. Set `networkMode: 'always'` for server prefetches so requests are never paused.
- **Suspense**: Prefer `useSuspenseQuery` for routes already using Suspense. Do not combine with `enabled`; gate rendering instead.
- **Testing**: Use `@testing-library/react` + `@tanstack/react-query/testing` helpers and mock network with MSW. Reset QueryClient between tests to avoid cache bleed.

---

## Dependencies

**Required**:
- `@tanstack/react-query@5.90.12` - Core library
- `react@18.0.0+` - Uses useSyncExternalStore hook
- `react-dom@18.0.0+` - React DOM renderer

**Recommended**:
- `@tanstack/react-query-devtools@5.91.1` - Visual debugger (dev only)
- `@tanstack/eslint-plugin-query@5.91.2` - ESLint rules for best practices
- `typescript@5.2.0+` - For type safety and inference

**Optional**:
- `@tanstack/query-sync-storage-persister` - Persist cache to localStorage
- `@tanstack/query-async-storage-persister` - Persist to AsyncStorage (React Native)

---

## Official Documentation

- **TanStack Query Docs**: https://tanstack.com/query/latest
- **React Integration**: https://tanstack.com/query/latest/docs/framework/react/overview
- **v5 Migration Guide**: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
- **API Reference**: https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
- **Context7 Library ID**: `/websites/tanstack_query`
- **GitHub Repository**: https://github.com/TanStack/query
- **Discord Community**: https://tlinz.com/discord

---

## Package Versions (Verified 2025-12-09)

```json
{
  "dependencies": {
    "@tanstack/react-query": "^5.90.12"
  },
  "devDependencies": {
    "@tanstack/react-query-devtools": "^5.91.1",
    "@tanstack/eslint-plugin-query": "^5.91.2"
  }
}
```

**Verification:**
- `npm view @tanstack/react-query version` → 5.90.12
- `npm view @tanstack/react-query-devtools version` → 5.91.1
- `npm view @tanstack/eslint-plugin-query version` → 5.91.2
- Last checked: 2025-12-09

---

## Production Example

This skill is based on production patterns used in:
- **Build Time**: ~6 hours research + development
- **Errors Prevented**: 8 (all documented v5 migration issues)
- **Token Efficiency**: ~65% savings vs manual setup
- **Validation**: ✅ All patterns tested with TypeScript strict mode

---

## Troubleshooting

### Problem: "useQuery is not a function" or type errors
**Solution**: Ensure you're using v5 object syntax:
```tsx
// ✅ Correct:
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

// ❌ Wrong (v4 syntax):
useQuery(['todos'], fetchTodos)
```

### Problem: Callbacks (onSuccess, onError) not working on queries
**Solution**: Removed in v5. Use `useEffect` or move to mutations:
```tsx
// ✅ For queries:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // Handle success
  }
}, [data])

// ✅ For mutations (still work):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => { /* ... */ },
})
```

### Problem: isLoading always false even during initial load
**Solution**: Use `isPending` instead:
```tsx
const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = no data yet
// isLoading = isPending && isFetching
// isFetching = any fetch in progress
```

### Problem: cacheTime option not recognized
**Solution**: Renamed to `gcTime` in v5:
```tsx
gcTime: 1000 * 60 * 60 // 1 hour
```

### Problem: useSuspenseQuery with enabled option gives type error
**Solution**: `enabled` not available with suspense. Use conditional rendering:
```tsx
{id && <TodoComponent id={id} />}
```

### Problem: Data not refetching after mutation
**Solution**: Invalidate queries in `onSuccess`:
```tsx
onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}
```

### Problem: Infinite query requires initialPageParam
**Solution**: Always provide `initialPageParam` in v5:
```tsx
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // Required
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
```

### Problem: keepPreviousData not working
**Solution**: Replaced with `placeholderData`:
```tsx
import { keepPreviousData } from '@tanstack/react-query'

useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: keepPreviousData,
})
```

---

## Complete Setup Checklist

Use this checklist to verify your setup:

- [ ] Installed @tanstack/react-query@5.90.12+
- [ ] Installed @tanstack/react-query-devtools (dev dependency)
- [ ] Created QueryClient with configured defaults
- [ ] Wrapped app with QueryClientProvider
- [ ] Added ReactQueryDevtools component
- [ ] Created first query using object syntax
- [ ] Tested isPending and error states
- [ ] Created first mutation with onSuccess handler
- [ ] Set up query invalidation after mutations
- [ ] Configured staleTime and gcTime appropriately
- [ ] Using array queryKey consistently
- [ ] Throwing errors in queryFn
- [ ] No v4 syn

…

_[truncated — view full skill on GitHub]_

How to use

  1. Copy the skill content above
  2. Create a .claude/skills directory in your project
  3. Save as .claude/skills/claude-skills-tanstack-query.md
  4. Use /claude-skills-tanstack-query in Claude Code to invoke this skill

Claude Code Skills Collection

170 production-ready skills for Claude Code CLI

Version 3.3.1 | Last Updated: 2026-05-14

<div align="center">

🔌 Platform Support

This repository uses Claude Plugin Patterns — natively supported by:

PlatformStatusNotes
Claude CodeNativeFull marketplace support
Factory DroidNativeFull marketplace support
</div> **For all other Platforms like opencode, codex and others, you can use https://github.com/enulus/OpenPackage **

A curated collection of battle-tested skills for building modern web applications with Cloudflare, AI integrations, React, Tailwind, and more.

PS: if skills.sh warns about any skill: Their scan process is a outdated LLM which flags newest versions pins (like in ZOD) as non existent and by that potentially malicous.


Quick Start

Marketplace Installation (Recommended)

# Add the marketplace
/plugin marketplace add https://github.com/secondsky/claude-skills

# Install individual skills as needed
/plugin install cloudflare-d1@claude-skills
/plugin install tailwind-v4-shadcn@claude-skills
/plugin install ai-sdk-core@claude-skills

See MARKETPLACE.md for complete catalog of all 170 skills.

Bulk Installation (Contributors)

# Clone the repository
git clone https://github.com/secondsky/claude-skills.git
cd claude-skills

# Install all 170 skills at once
./scripts/install-all.sh

# Or install individual skills
./scripts/install-skill.sh cloudflare-d1

Repository Structure

This repository contains 170 production-tested skills for Claude Code, each focused on a specific technology or capability.

Individual Skills: Each skill is a standalone unit with:

  • SKILL.md - Core knowledge and guidance
  • Templates - Working code examples
  • References - Extended documentation
  • Scripts - Helper utilities

Installation Options:

  1. Individual - Install only the skills you need via marketplace
  2. Bulk - Install all 170 skills using ./scripts/install-all.sh

Available Skills (170 Individual Skills)

Each skill is individually installable. Install only the skills you need.

Full Catalog: See MARKETPLACE.md for detailed listings.

Categories

CategorySkillsExamples
tooling29turborepo, plan-interview, code-review
frontend26nuxt-v4, nuxt-v5, tailwind-v4-shadcn, tanstack-query, nuxt-studio, maz-ui, threejs
cloudflare21cloudflare-d1, cloudflare-workers-ai, cloudflare-agents
ai20openai-agents, claude-api, ai-sdk-core
api16api-design-principles, graphql-implementation
web10hono-routing, firecrawl-scraper, web-performance
mobile7swift-best-practices, react-native-app, react-native-skills
database6drizzle-orm-d1, neon-vercel-postgres, supabase-postgres-best-practices
security6csrf-protection, access-control-rbac
auth4better-auth
testing4vitest-testing, playwright-testing
design4design-review, design-system-creation
woocommerce4woocommerce-backend-dev
cms4hugo, sveltia-cms, wordpress-plugin-core
architecture3microservices-patterns, architecture-patterns
data3sql-query-optimization, recommendation-engine
seo2seo-optimizer, seo-keyword-cluster-builder
documentation1technical-specification

How It Works

Auto-Discovery

Claude Code automatically checks ~/.claude/skills/ for relevant skills before planning tasks:

User: "Set up a Cloudflare Worker with D1 database"
           ↓
Claude: [Checks skills automatically]
           ↓
Claude: "Found cloudflare-d1 skills.
         These prevent 12 documented errors. Use them?"
           ↓
User: "Yes"
           ↓
Result: Production-ready setup, zero errors, ~65% token savings

Note: Due to token limits, not all skills may be visible at once. See ⚠️ Important: Token Limits below.

Skill Structure

Each skill includes:

skills/[skill-name]/
├── SKILL.md              # Complete documentation
├── .claude-plugin/
│   └── plugin.json       # Plugin metadata
├── templates/            # Ready-to-copy templates
├── scripts/              # Automation scripts
└── references/           # Extended documentation

Recent Additions

May 2026

Supply Chain Security (cross-cutting):

  • dependency-upgrade expanded with Socket CLI integration — proactive malicious package detection, typosquatting alerts, and CI/CD security gates. New 418-line reference guide, 2 GitHub Actions templates, and expanded supply chain security comparison (3 tools)
  • 31 skills now include "Secure Installation" guidance — contextually-tailored security sections across all high-risk skill categories (scaffolding, MCP/agent SDKs, multi-provider installs, Docker, CI/CD). Covers 8 Bun skills, 5 Nuxt skills, 6 Cloudflare skills, 4 AI/agent skills, and 8 frontend/tooling skills
  • Supply chain security is now a first-class cross-cutting concern woven into the skill collection — not a standalone topic

February - April 2026

Full-Stack Frameworks:

  • nuxt-v5 (v1.0.0) - Full Nuxt 5 support with 4 skills (core, data, server, production), 3 diagnostic agents, and interactive setup wizard
  • supabase-postgres-best-practices - 30 Postgres optimization rules from Supabase across 8 categories
  • threejs (v1.0.0) - 3D web graphics: scenes, geometries, shaders, animations, post-processing

Infrastructure:

  • JSON schema validation - Automated plugin.json validation with CI support
  • GitHub issue templates - Skill-specific issue templates for bug reports, feature requests, and submissions

Plugin Enhancements:

  • mutation-testing - Added Bun native runner support
  • dependency-upgrade - Added supply chain security content

December 2025 - January 2026

Frontend Expansion:

  • nuxt-studio (v1.0.0) - Visual CMS for Nuxt Content with live preview, OAuth auth, and R2 storage integration
  • maz-ui (v1.0.0) - 50+ Vue/Nuxt components with theming, i18n, form generation, and 14 composables

Developer Workflow:

  • plan-interview (v2.0.0) - Adaptive interview-driven spec generation with autonomous quality review
  • turborepo (v2.8.0) - Updated to official Vercel skill with enhanced monorepo build optimization

Mobile Development:

  • react-native-skills (v1.0.0) - React Native & Expo best practices with performance optimization patterns

Enhanced Authentication:

  • better-auth (v2.2.0) - Expanded to 18 framework integrations with 30+ authentication plugins

⚠️ Important: Token Limits

Skill Visibility Constraint

Claude Code has a 15,000 character limit for the total size of skill descriptions in the system prompt. This limit also applies to commands and agents.

What this means:

  • Not all 170 skills may be visible in Claude's context at once
  • Skills are loaded based on relevance and available token budget
  • You can verify how many skills Claude currently sees by asking: "How many skills do you see in your system prompt?"

Checking Visible Skills

To verify which skills are currently loaded:

# Ask Claude Code directly
"Check what skills/plugins you see in your system prompt"

Claude will report something like: "85 of 170 skills visible due to token limits"

Workaround: Increase Token Budget

You can double the headroom for skill descriptions by setting an environment variable:

# Increase limit to 30,000 characters
export SLASH_COMMAND_TOOL_CHAR_BUDGET=30000

# Then launch Claude Code
claude

This gives you approximately 2x more skill visibility in the system prompt.

Note: This is a temporary workaround. The Claude Code team is working on better solutions for skill discovery and loading.


Token Efficiency

MetricManual SetupWith SkillsSavings
Average Tokens12,000-15,0004,000-5,000~65%
Typical Errors2-4 per service0 (prevented)100%
Setup Time2-4 hours15-45 minutes~80%

Across all 170 skills: 400+ documented errors prevented.


Contributing

Prerequisites for Contributors

Install the official plugin development toolkit:

/plugin install plugin-dev@claude-code-marketplace

This provides:

  • /plugin-dev:create-plugin command (8-phase guided workflow)
  • 7 comprehensive skills (hooks, MCP, structure, agents, commands, skills)
  • 2 specialized agents (agent-creator, plugin-validator)

Quick Steps

  1. Create skill directory in plugins/
  2. Add SKILL.md with YAML frontmatter
  3. Run ./scripts/sync-plugins.sh
  4. Submit pull request

See CONTRIBUTING.md and PLUGIN_DEV_BEST_PRACTICES.md for detailed guidelines.


Documentation

DocumentPurpose
START_HERE.mdStart here! Quick navigation guide
PLUGIN_DEV_BEST_PRACTICES.mdRepository-specific best practices (marketplace, budget, quality)
MARKETPLACE.mdFull skill catalog and installation guide
MARKETPLACE_MANAGEMENT.mdTechnical infrastructure (plugin.json, scripts, validation)
CLAUDE.mdProject context and development standards
CONTRIBUTING.mdContribution guidelines

Links


Built with ❤️ by Claude Skills Maintainers

View source on GitHub