Tanstack Query
Mindrally/skills
Best practices for TanStack Query v5 (React Query) server state management, including query key factories, the queryOptions helper, mutations, optimistic updates, infinite queries, and Suspense mode.
TanStack Query v5 (React Query) server state management. An agent skill from secondsky/claude-skills.
$ npx skills add secondsky/claude-skills --skill tanstack-query -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install secondsky/claude-skills tanstack-query --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/secondsky/claude-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/plugins/tanstack-query/skills/tanstack-query .claude/skills/tanstack-query && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "tanstack-query" agent skill from https://github.com/secondsky/claude-skills/tree/main/plugins/tanstack-query/skills/tanstack-query into .claude/skills/tanstack-query/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "tanstack-query", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/secondsky/claude-skills/tree/main/plugins/tanstack-query/skills/tanstack-queryType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add secondsky/claude-skills --skill tanstack-query -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install secondsky/claude-skills tanstack-query --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/secondsky/claude-skills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/plugins/tanstack-query/skills/tanstack-query .agents/skills/tanstack-query && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "tanstack-query" agent skill from https://github.com/secondsky/claude-skills/tree/main/plugins/tanstack-query/skills/tanstack-query into .agents/skills/tanstack-query/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "tanstack-query", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add secondsky/claude-skills --skill tanstack-query -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install secondsky/claude-skills tanstack-query --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/secondsky/claude-skills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/plugins/tanstack-query/skills/tanstack-query .cursor/skills/tanstack-query && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "tanstack-query" agent skill from https://github.com/secondsky/claude-skills/tree/main/plugins/tanstack-query/skills/tanstack-query into .cursor/skills/tanstack-query/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "tanstack-query", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/secondsky/claude-skills.git --path plugins/tanstack-query/skills/tanstack-query--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add secondsky/claude-skills --skill tanstack-query -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install secondsky/claude-skills tanstack-query --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/secondsky/claude-skills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/plugins/tanstack-query/skills/tanstack-query .gemini/skills/tanstack-query && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "tanstack-query" agent skill from https://github.com/secondsky/claude-skills/tree/main/plugins/tanstack-query/skills/tanstack-query into .gemini/skills/tanstack-query/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "tanstack-query", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install secondsky/claude-skills tanstack-queryInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add secondsky/claude-skills --skill tanstack-query -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/secondsky/claude-skills.git skills-src && mkdir -p .github/skills && cp -r skills-src/plugins/tanstack-query/skills/tanstack-query .github/skills/tanstack-query && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "tanstack-query" agent skill from https://github.com/secondsky/claude-skills/tree/main/plugins/tanstack-query/skills/tanstack-query into .github/skills/tanstack-query/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "tanstack-query", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add secondsky/claude-skills --skill tanstack-query -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install secondsky/claude-skills tanstack-query --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/secondsky/claude-skills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/plugins/tanstack-query/skills/tanstack-query .opencode/skills/tanstack-query && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "tanstack-query" agent skill from https://github.com/secondsky/claude-skills/tree/main/plugins/tanstack-query/skills/tanstack-query into .opencode/skills/tanstack-query/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "tanstack-query", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
tanstack-queryTanStack Query v5 (React Query) server state management. An agent skill from secondsky/claude-skills.
Tanstack Query is an agent skill from secondsky/claude-skills. TanStack Query v5 (React Query) server state management. Use for data fetching, caching, mutations, or encountering v4 migration, stale data, invalidation errors.
Its SKILL.md is about 7.9k tokens, which your agent loads only when the skill is triggered. The skill folder holds 38 other files, including scripts, reference files and assets (for example `examples/README.md`, `references/advanced-setup.md` and `references/best-practices.md`).
It sits in Frontend & Design, covering State management and Caching. It works with TanStack and React. The repository describes itself as: Production-ready skills for Claude Code CLI - Cloudflare, React, Tailwind v4, and AI integrations. The licence is MIT.
7 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 8837836. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships 1 file in scripts/, which the agent can run.
Shell commands in SKILL.md call:
pnpmnpmbunFrom the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
tanstack.comtlinz.comgithub.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Tanstack Query loads about 7.9k tokens when it runs, and up to ~23k if it reads all its reference files. Until then it costs about 44 tokens; SKILL.md has 1,612 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.
The full file from secondsky/claude-skills at commit 8837836, republished under its MIT licence (© secondsky). 1,612 words, ~7,896 tokens.
.claude/skills/tanstack-query/SKILL.md (or your agent's skills folder). This skill also uses 35 other files; get the full folder from GitHub.Status: Production Ready ✅ Last Updated: 2026-08-03 Dependencies: React 18.0+ (18.3+ recommended), TypeScript 4.9+ (5.x preferred) Latest Versions: @tanstack/react-query@5.101.4, @tanstack/react-query-devtools@5.101.4, @tanstack/eslint-plugin-query@5.101.4
# 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@latestWhy this matters:
// 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:
QueryClientProviderstaleTime to avoid excessive refetches (default is 0)gcTime (not cacheTime - renamed in v5)Know the defaults (v5):
staleTime: 0 → data is immediately stale, so refetches on mount/focus unless you raise itgcTime: 5 * 60 * 1000 → inactive data is garbage-collected after 5 minutesretry: 3 in browsers, retry: 0 on the serverrefetchOnWindowFocus: true and refetchOnReconnect: truenetworkMode: 'online' (requests pause while offline). Switch to 'always' for SSR/prefetch where you don't want cancellation. citeturn1search0turn1search1// 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:
useQuery({ queryKey, queryFn })isPending (not isLoading - that now means "pending AND fetching")// 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:
onSuccess, onError, onSettled) - queries don'tinvalidateQueries triggers background refetch# 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-queryPackage 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 mistakesVersion requirements:
useSyncExternalStore)// 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 refetch0 (default): Data is immediately stale, refetches on mount/focus1000 * 60 * 5: Data fresh for 5 min, no refetch during this timeInfinity: Data never stale, manual invalidation onlygcTime: How long unused data stays in cache1000 * 60 * 5 (default): 5 minutesInfinity: 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 developmentrefetchOnMount: true - Ensures fresh data when component mountsrefetchOnReconnect: true - Refetch after network reconnect// 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:
DevTools configuration:
initialIsOpen={false} - Collapsed by defaultbuttonPosition="bottom-right" - Where to show toggle buttonFor 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.
✅ Use object syntax for all hooks
// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })✅ Use array query keys
queryKey: ['todos'] // List
queryKey: ['todos', id] // Detail
queryKey: ['todos', { filter }] // Filtered✅ Configure staleTime appropriately
staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches✅ Use isPending for initial loading state
if (isPending) return <Loading />
// isPending = no data yet AND fetching✅ Throw errors in queryFn
if (!response.ok) throw new Error('Failed')✅ Invalidate queries after mutations
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}✅ Use queryOptions factory for reusable patterns
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)✅ Use gcTime (not cacheTime)
gcTime: 1000 * 60 * 60 // 1 hour✅ Know your status flags
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 use v4 array/function syntax
// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌
// v5 (correct):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅❌ Never use query callbacks (onSuccess, onError, onSettled in queries)
// 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
// 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"
// 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
// v5 requires this:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})❌ Never use enabled with useSuspenseQuery
// Not allowed:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not available with suspense
})
// Use conditional rendering instead:
{id && <TodoComponent id={id} />}This skill prevents 8+ documented v5 migration issues. The most critical errors include:
isPending vs isLoading status changescacheTime renamed to gcTimeinitialPageParam required for infinite querieskeepPreviousData replaced with placeholderDataFor complete error catalog with before/after examples: Load references/top-errors.md when encountering errors or debugging v5 migration issues.
Essential configuration files: package.json, tsconfig.json, .eslintrc.cjs
Key requirements:
For complete configuration templates: Load references/configuration-files.md when setting up new projects or troubleshooting build/type errors.
// 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
// 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
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)
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
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:
signal to fetch for proper cleanupconst { 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.
Complete, copy-ready code examples:
package.json - Dependencies with exact versionsquery-client-config.ts - QueryClient setup with best practicesprovider-setup.tsx - App wrapper with QueryClientProvideruse-query-basic.tsx - Basic useQuery hook patternuse-mutation-basic.tsx - Basic useMutation hookuse-mutation-optimistic.tsx - Optimistic update patternuse-infinite-query.tsx - Infinite scroll patterncustom-hooks-pattern.tsx - Reusable query hooks with queryOptionserror-boundary.tsx - Error boundary with query resetdevtools-setup.tsx - DevTools configurationExample Usage:
# 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 . pnpmDeep-dive documentation loaded when needed:
advanced-setup.md - Custom hooks, mutations, optimistic updates, DevTools, error boundariesconfiguration-files.md - Complete package.json, tsconfig.json, .eslintrc.cjs templatesv4-to-v5-migration.md - Complete v4 → v5 migration guidebest-practices.md - Request waterfalls, caching strategies, performancecommon-patterns.md - Reusable queries, optimistic updates, infinite scrollofficial-guides-map.md - When to open each official doc and what it coverstypescript-patterns.md - Type safety, generics, type inferencetesting.md - Testing with MSW, React Testing Librarytop-errors.md - All 8+ errors with solutionsexamples/README.md - Index of top 10 scenarios with official linksbasic.tsx - Minimal list querybasic-graphql-request.tsx - GraphQL client + selectoptimistic-update.tsx - onMutate snapshot/rollbackpagination.tsx - paginated list with placeholderDatainfinite-scroll.tsx - useInfiniteQuery + IntersectionObserverprefetching.tsx - prefetch on hover before navigationsuspense.tsx - useSuspenseQuery + boundarydefault-query-function.tsx - global fetcher using queryKeynextjs-app-router.tsx - App Router prefetch + hydrate (networkMode: 'always')react-native.tsx - offline-first with AsyncStorage persisterWhen Claude should load these:
advanced-setup.md - When implementing custom query hooks, mutations, or error boundariesconfiguration-files.md - When setting up new projects or troubleshooting build/type errorsv4-to-v5-migration.md - When migrating existing React Query v4 projectbest-practices.md - When optimizing performance or avoiding waterfallscommon-patterns.md - When implementing specific features (infinite scroll, etc.)typescript-patterns.md - When dealing with TypeScript errors or type inferencetesting.md - When writing tests for components using TanStack Querytop-errors.md - When encountering errors not covered in main SKILL.md// 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:
// ❌ 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!
}// ❌ 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:
@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-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.dehydrate/HydrationBoundary on the server and QueryClientProvider on the client. Set networkMode: 'always' for server prefetches so requests are never paused.useSuspenseQuery for routes already using Suspense. Do not combine with enabled; gate rendering instead.@testing-library/react + @tanstack/react-query/testing helpers and mock network with MSW. Reset QueryClient between tests to avoid cache bleed.Required:
@tanstack/react-query@5.101.4 - Core libraryreact@18.0.0+ - Uses useSyncExternalStore hookreact-dom@18.0.0+ - React DOM rendererRecommended:
@tanstack/react-query-devtools@5.101.4 - Visual debugger (dev only)@tanstack/eslint-plugin-query@5.101.4 - ESLint rules for best practicestypescript@5.2.0+ - For type safety and inferenceOptional:
@tanstack/query-sync-storage-persister - Persist cache to localStorage@tanstack/query-async-storage-persister - Persist to AsyncStorage (React Native)/websites/tanstack_query{
"dependencies": {
"@tanstack/react-query": "^5.101.4"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.101.4",
"@tanstack/eslint-plugin-query": "^5.101.4"
}
}Verification:
npm view @tanstack/react-query version → 5.101.4npm view @tanstack/react-query-devtools version → 5.101.4npm view @tanstack/eslint-plugin-query version → 5.101.4This skill is based on production patterns used in:
Solution: Ensure you're using v5 object syntax:
// ✅ Correct:
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// ❌ Wrong (v4 syntax):
useQuery(['todos'], fetchTodos)Solution: Removed in v5. Use useEffect or move to mutations:
// ✅ For queries:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// Handle success
}
}, [data])
// ✅ For mutations (still work):
useMutation({
mutationFn: addTodo,
onSuccess: () => { /* ... */ },
})Solution: Use isPending instead:
const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = no data yet
// isLoading = isPending && isFetching
// isFetching = any fetch in progressSolution: Renamed to gcTime in v5:
gcTime: 1000 * 60 * 60 // 1 hourSolution: enabled not available with suspense. Use conditional rendering:
{id && <TodoComponent id={id} />}Solution: Invalidate queries in onSuccess:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}Solution: Always provide initialPageParam in v5:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // Required
getNextPageParam: (lastPage) => lastPage.nextCursor,
})Solution: Replaced with placeholderData:
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})Use this checklist to verify your setup:
Questions? Issues?
references/top-errors.md for complete error solutions© secondsky, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 35 other files (scripts, references, assets) in plugins/tanstack-query/skills/tanstack-query of secondsky/claude-skills.
Open the folder on GitHubat commit 8837836
Tanstack Query next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Tanstack Query this skillsecondsky/claude-skills | 227 | — | ~7.9k | Automated safety check: Pass | MIT | |
| Tanstack QueryMindrally/skills | 267 | — | ~3.6k | Automated safety check: Pass | Apache-2.0 | |
| Olore Tanstack Query 3olorehq/olore | 103 | — | ~700 | Automated safety check: Pass | MIT | |
| Olore Tanstack Query Latestolorehq/olore | 103 | — | ~487 | Automated safety check: Pass | MIT | |
| Tanstack Query Best PracticesDeckardGer/tanstack-agent-skills | 222 | 1 repos | ~1.2k | Automated safety check: Pass | MIT | |
| Olore Tanstack Query 4olorehq/olore | 103 | — | ~583 | Automated safety check: Pass | MIT |
Mindrally/skills
Best practices for TanStack Query v5 (React Query) server state management, including query key factories, the queryOptions helper, mutations, optimistic updates, infinite queries, and Suspense mode.
olorehq/olore
Local TanStack Query v3 (React Query) documentation reference.
olorehq/olore
Local TanStack Query documentation reference (latest). An agent skill from olorehq/olore.
DeckardGer/tanstack-agent-skills
TanStack Query (React Query) best practices for data fetching, caching, mutations, and server state management.
olorehq/olore
Local TanStack Query documentation reference (v4). An agent skill from olorehq/olore.
bskimball/tanstack-hono
Powerful asynchronous state management, server-state utilities, and data fetching for TS/JS, React, Vue, Solid, Svelte & Angular.
secondsky/claude-skills
TanStack AI (alpha) provider-agnostic type-safe chat with streaming for OpenAI, Anthropic, Gemini, Ollama.
secondsky/claude-skills
AutoAnimate (@formkit/auto-animate) zero-config animations for React.
secondsky/claude-skills
MUI Base UI unstyled React components with Floating UI. An agent skill from secondsky/claude-skills.
secondsky/claude-skills
This skill should be used when the user asks to "upload images to Cloudflare", "implement direct creator upload", "configure image transformations", "optimize WebP/AVIF", "create image variants"…
secondsky/claude-skills
Deploy Next.js to Cloudflare Workers via the OpenNext adapter (@opennextjs/cloudflare).
secondsky/claude-skills
Cloudflare Sandboxes SDK for secure code execution in Linux containers at edge.
Categories
TanStack Query v5 (React Query) server state management. An agent skill from secondsky/claude-skills. Tanstack Query is an agent skill from secondsky/claude-skills. TanStack Query v5 (React Query) server state management.
Tanstack Query fits situations like: encountering v4 migration; invalidation errors.
Run `npx skills add secondsky/claude-skills --skill tanstack-query -a claude-code`. Or copy the skill folder (plugins/tanstack-query/skills/tanstack-query in secondsky/claude-skills) into .claude/skills/tanstack-query in your project. Claude Code loads it when a task matches its description.
Run `npx skills add secondsky/claude-skills --skill tanstack-query -a codex`. Or copy the skill folder (plugins/tanstack-query/skills/tanstack-query in secondsky/claude-skills) into .agents/skills/tanstack-query in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add secondsky/claude-skills --skill tanstack-query -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/tanstack-query, .gemini/skills/tanstack-query, .github/skills/tanstack-query and .opencode/skills/tanstack-query in your project.
Going by SKILL.md and its folder, Tanstack Query needs the command-line tools its instructions call (pnpm, npm and bun). Our summary lists: Node.js.
SKILL.md names 3 domains. As links in the text: tanstack.com, tlinz.com and github.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.
Tanstack Query is published under the MIT licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 7.9k tokens (SKILL.md is roughly 32k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 15k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Tanstack Query: Tanstack Query (Mindrally/skills, 267 stars), Olore Tanstack Query 3 (olorehq/olore, 103 stars), Olore Tanstack Query Latest (olorehq/olore, 103 stars) and Tanstack Query Best Practices (DeckardGer/tanstack-agent-skills, 222 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
secondsky (a GitHub user) maintains it in secondsky/claude-skills, which has 227 GitHub stars. The repository holds 169 skills in this directory. The repository was last updated on September 28, 2026.
Source: secondsky/claude-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.