skills/tanstack-query/SKILL.md
Use when debugging TanStack Query / React Query issues: v4→v5 migration errors (gcTime, isPending, throwOnError), infinite refetch loops, SSR hydration mismatches, choosing between React Query vs SWR, or optimistic update patterns not working. NOT for basic useQuery setup.
npx skillsauth add acedergren/agentic-tools tanstack-queryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Assumption: You know useQuery basics. This covers what breaks in production.
$ARGUMENTS: Query bug, migration issue, or caching decision to analyze
/tanstack-query infinite refetch loop on dashboard/tanstack-query v4 to v5 cacheTime issueNeed data fetching?
│
├─ Data from URL (search params, path) → DON'T use queries
│ └─ Use framework loaders (Next.js, Remix)
│ WHY: Queries cache by key, URL is already your cache key
│
├─ Derived/computed data → DON'T use queries
│ └─ Use useMemo or Zustand
│ WHY: No server, no stale data, no refetch needed
│
├─ Form state → DON'T use queries
│ └─ Use React Hook Form or controlled state
│
├─ WebSocket/realtime (> 1/sec) → DON'T use queries
│ └─ Use Zustand; queries are designed for request/response, not streaming
│
└─ REST/GraphQL server state → USE queries ✅
The trap: Developers use React Query for everything. It's a server cache, not a state manager.
| Update frequency | Recommended staleTime | | ---------------- | --------------------- | | Real-time (>1/sec) | WebSocket + Zustand instead | | Frequent (<1/min) | 30s–1min | | Moderate (5–30min) | 5min (default) | | Infrequent (>1hr) | 30min+ | | Critical (money, auth) | 0 (always fresh) |
cacheTime Renamed to gcTimeFailure mode: Silent — code runs, TypeScript doesn't error, cache garbage-collects immediately.
// WRONG - silently ignored in v5
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, cacheTime: 10 * 60 * 1000 })
// CORRECT
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, gcTime: 10 * 60 * 1000 })
Debug signal: DevTools shows 0ms gcTime despite setting 10 minutes.
isLoading Removed → Use isPendingFailure mode: if (isLoading) evaluates falsy (undefined), spinner never shows.
// WRONG - isLoading is undefined in v5
const { isLoading } = useQuery(...)
// CORRECT
const { isPending } = useQuery(...)
Semantic difference: isPending stays true during refetches with cached data — isLoading did not. Causes "stale data + spinner simultaneously" if naively swapped.
keepPreviousData → placeholderDataFailure mode: Pagination flickers on page change.
// WRONG
useQuery({ queryKey: ['todos', page], keepPreviousData: true })
// CORRECT - function form required
useQuery({
queryKey: ['todos', page],
placeholderData: (previousData) => previousData,
})
Failure mode: Silent runtime error when using any types.
// WRONG - void return
queryFn: async () => { await api.deleteTodo(id) }
// CORRECT
queryFn: async () => { await api.deleteTodo(id); return { success: true } }
Cause: Object or array reference in queryKey — new reference on every render triggers new query.
// WRONG - object in key = new reference each render = infinite loop
useQuery({ queryKey: ['user', user], queryFn: () => fetchUser(user.id) })
// CORRECT - use stable primitives
useQuery({ queryKey: ['user', user.id], queryFn: () => fetchUser(user.id) })
Detection: Network tab shows identical requests >10/sec. React DevTools Profiler shows constant re-renders.
Fallback (when key must contain object):
const stableKey = useMemo(() => ['user', user], [user.id])
useQuery({ queryKey: stableKey, queryFn: () => fetchUser(user.id), structuralSharing: false })
Cause: staleTime: Infinity — data never marked stale regardless of server changes.
Detection: Network tab shows zero requests after initial load. Users report "data doesn't update" but devs can't reproduce (devs refresh frequently, clearing cache).
Fix: Use reasonable staleTime. If still stale: queryClient.invalidateQueries({ queryKey: ['your-key'] }).
Cause: queryClient.invalidateQueries() with no filter nukes entire cache → all queries refetch.
// WRONG - refetches 100 queries on every mutation
onSuccess: () => { queryClient.invalidateQueries() }
// CORRECT - targeted
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['user', userId] }) }
Mutation completes...
│
├─ Simple list append/prepend → Optimistic (useMutationState)
│ └─ Add todo, add comment — no complex logic needed
│
├─ Complex computed data → Invalidation
│ └─ Aggregates, filters, sorts — let server compute
│
├─ Risk of conflicts (multi-user) → Invalidation
│ └─ Optimistic update may be wrong; let server resolve
│
└─ Must feel instant → Optimistic + rollback on error
└─ Toggle like, toggle favorite
| Prefer React Query | Prefer SWR | | ------------------ | ---------- | | Fine-grained gc/stale control | Simpler API (less config) | | Complex invalidation patterns | Smaller bundle size priority | | Optimistic updates with rollback | Next.js (first-party support) | | Infinite queries / pagination | Simple dashboard use case | | Already in TanStack ecosystem | |
Server renders "Loading...", client has cached data → hydration error.
// app/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
export default async function Page() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<TodoList />
</HydrationBoundary>
)
}
// components/TodoList.tsx ('use client')
export function TodoList() {
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// No isPending check — data guaranteed from server prefetch
return <div>{data.map(...)}</div>
}
Hydration mismatch fallback: Pass as initialData via props instead of prefetch.
// Find refetch loops — add to QueryClient defaultOptions
onSuccess: (data, query) => { console.count(`Refetch: ${query.queryKey}`) }
// Count > 10 in 1 second = infinite loop
// Check cache state
const state = queryClient.getQueryState(['todos'])
console.log(state?.isInvalidated)
// Nuclear cache clear
queryClient.removeQueries({ queryKey: ['your-key'] })
queryClient.refetchQueries({ queryKey: ['your-key'] })
// Or: queryClient.clear()
# Find v4 property names still in codebase
grep -r "cacheTime\|isLoading\|keepPreviousData" src/
Add <ReactQueryDevtools initialIsOpen={false} /> to visualize cache state, refetch counts, and staleness.
READ references/v5-features.md when using 3+ v5-specific features simultaneously (useMutationState, throwOnError, infinite queries, suspense mode).
READ references/migration-guide.md when migrating a codebase with 10+ query usages or running codemods.
Do NOT load references for single breaking change fixes, basic troubleshooting, or simple optimistic updates — all covered above.
development
--- name: api-audit description: "Use when auditing API routes for schema drift, missing auth, or validation gaps. Scans routes against shared TypeScript types to find mismatches, missing middleware, and undocumented endpoints. Read-only — produces a severity-grouped report. Keywords: audit routes, schema drift, auth gaps, missing validation, type mismatch, orphaned schemas. Triggers on "audit API routes" or "find schema drift"." --- # API Route & Type Audit Skill ## When to Use Load this skil
development
Use when drafting, translating, polishing, or reviewing Swedish text so it sounds natural, fluent, contemporary, and appropriate for its audience. Triggers include "write better Swedish", "make this sound natural in Swedish", "translate into Swedish", "polish this Swedish", "tech company Swedish", "contemporary Swedish words", "Swedish developer docs", and "avoid Anglicisms".
development
Use when working with shadcn-svelte components, TanStack Table in Svelte 5, or Tailwind v4.1. Covers non-obvious reactivity bugs, library selection trade-offs, and migration pitfalls not in the official docs. Keywords: shadcn-svelte, TanStack Table, Tailwind v4.1, Svelte 5 runes, bits-ui, superforms, data table, svelte-check.
data-ai
Use when mapping IDCS claims to org membership after OAuth login succeeds. Covers mapProfileToUser, session.create.before, session.create.after hooks, MERGE INTO upserts, tenant-org mapping, and first-admin bootstrap. Keywords: IDCS groups, org_members, provisioning, session hooks, tenant map, MERGE INTO.