jackspace/claudeskillz/tanstack-query

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

TanStack Query v5 (React Query) server-state management for React: QueryClient and QueryClientProvider setup, custom useQuery hooks, useMutation with error handling, useInfiniteQuery pagination, useSuspenseQuery, query keys, staleTime/gcTime caching, invalidation, prefetching, optimistic updates, and React Query Devtools. Use when adding data fetching to a React app, debugging stale data or caching problems, fixing request waterfalls or background refetching, or migrating React Query v4 to v5 breaking changes: cacheTime renamed to gcTime, isLoading renamed to isPending, required object syntax, initialPageParam required, keepPreviousData removed in favor of placeholderData, and onSuccess/onError query callbacks removed.

Install

npx vskill@latest install jackspace/claudeskillz/tanstack-query

SKILL.md

TanStack Query (React Query) v5

Status: Production Ready ✅ Last Updated: 2025-10-22 Dependencies: React 18.0+, TypeScript 4.7+ (recommended) Latest Versions: @tanstack/react-query@5.90.5, @tanstack/react-query-devtools@5.90.2


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 (!response.ok) {
throw new Error(`Failed to fetch todos: ${response.statusText}`)
}
return response.json()
}
export async function fetchTodoById(id: number): Promise<Todo> {
const response = await fetch(`/api/todos/${id}`)
if (!response.ok) {
throw new Error(`Failed to fetch todo ${id}: ${response.statusText}`)
}
return response.json()
}
// src/hooks/useTodos.ts - Query hooks
import { useQuery, queryOptions } from '@tanstack/react-query'
import { fetchTodos, fetchTodoById } from '../api/todos'
// Query options factory (v5 pattern for reusability)
export const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60, // 1 minute
})
export function useTodos() {
return useQuery(todosQueryOptions)
}
export function useTodo(id: number) {
return useQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodoById(id),
enabled: !!id, // Only fetch if id is truthy
})
}

Why use queryOptions factory:

  • Type inference works perfectly
  • Reusable across useQuery, useSuspenseQuery, prefetchQuery
  • Consistent queryKey and queryFn everywhere
  • Easier to test and maintain

Query key structure:

  • ['todos'] - List of all todos
  • ['todos', id] - Single todo detail
  • ['todos', 'filters', { status: 'completed' }] - Filtered list
  • More specific keys are subsets (invalidating ['todos'] invalidates all)
Step 5: Implement Mutations with Optimistic Updates
tsx
// src/hooks/useTodoMutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Todo } from '../api/todos'
type AddTodoInput = {
title: string
}
type UpdateTodoInput = {
id: number
completed: boolean
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo: AddTodoInput) => {
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()
},
// Optimistic update
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot previous value
const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])
// Optimistically update
queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [
...old,
{ id: Date.now(), ...newTodo, completed: false },
])
// Return context with snapshot
return { previousTodos }
},
// Rollback on error
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos)
},
// Always refetch after error or success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
export function useUpdateTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, completed }: UpdateTodoInput) => {
const response = await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed }),
})
if (!response.ok) throw new Error('Failed to update todo')
return response.json()
},
onSuccess: (updatedTodo) => {
// Update the specific todo in cache
queryClient.setQueryData<Todo>(['todos', updatedTodo.id], updatedTodo)
// Invalidate list to refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
export function useDeleteTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/api/todos/${id}`, { method: 'DELETE' })
if (!response.ok) throw new Error('Failed to delete todo')
},

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
14
Repository forks
6
7-day trend score
14

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 11 May 2026, 5 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/jackspace/claudeskillz/tanstack-query/badge)](https://verified-skill.com/skills/jackspace/claudeskillz/tanstack-query)

Something wrong with this skill or its page? Tell us.