Loading...
Loading...
Use when configuring QueryClient, implementing mutations, debugging performance, or adding optimistic updates with @tanstack/react-query in Next.js App Router. Covers factory patterns, query keys, cache invalidation, observer debugging, HydrationBoundary, multi-layer caching. Keywords TanStack Query, useSuspenseQuery, useQuery, useMutation, invalidateQueries, staleTime, gcTime, refetch, hydration.
npx skill4agent add gohypergiant/agent-skills accelint-tanstack-query-best-practicessetQueryData(key, (old) => ({ ...old, changed: value }))setQueryData(key, newValue)query-client-setup.mdserver-integration.mdquery-keys.mdserver-integration.mdmutations-and-updates.mdpatterns-and-pitfalls.mdpatterns-and-pitfalls.mdfundamentals.mdcaching-strategy.md| Data Type | staleTime | gcTime | refetchInterval | structuralSharing | Notes |
|---|---|---|---|---|---|
| Reference/Lookup | 1hr | Infinity | - | true | Countries, categories, static enums |
| User Profile | 5min | 10min | - | true | Changes infrequently, moderate freshness |
| Real-time Tracking | 5s | 30s | 5s | false | High update frequency, large payloads |
| Live Dashboard | 2s | 1min | 2s | Depends on size | Balance freshness vs performance |
| Detail View | 30s | 2min | - | true | Fetched on-demand, moderate caching |
| Search Results | 1min | 5min | - | true | Cacheable, not time-sensitive |
| Scenario | Pattern | When to Use |
|---|---|---|
| Form submission | Pessimistic | Multi-step forms, server validation required, error messages needed before proceeding |
| Toggle/checkbox | Optimistic | Binary state changes, low latency required, easy to rollback |
| Drag and drop | Optimistic | Immediate visual feedback essential, reordering operations, non-critical data |
| Batch operations | Pessimistic | Multiple items, partial failures possible, user needs confirmation of what succeeded |
| Life-critical ops | Pessimistic | Medical, financial, safety-critical systems where UI must match server reality |
| Audit trail required | Pessimistic | Compliance systems where operator actions must match logged events exactly |
// Recommended structure
export const keys = {
all: () => ['domain'] as const,
lists: () => [...keys.all(), 'list'] as const,
list: (filters: string) => [...keys.lists(), filters] as const,
details: () => [...keys.all(), 'detail'] as const,
detail: (id: string) => [...keys.details(), id] as const,
};
// Invalidation examples
queryClient.invalidateQueries({ queryKey: keys.all() }); // Invalidate everything
queryClient.invalidateQueries({ queryKey: keys.lists() }); // Invalidate all lists
queryClient.invalidateQueries({ queryKey: keys.detail(id) }); // Invalidate one item| Layer | Purpose | Invalidation Method | Cache Scope |
|---|---|---|---|
| Next.js use cache | Reduce database load | revalidateTag() or updateTag() | Cross-request, server-side |
| TanStack Query | Client-side state management | queryClient.invalidateQueries() | Per-browser-tab |
| Browser HTTP cache | Eliminate network requests | Cache-Control headers | Per-browser |
| Observer Count | Performance Impact | Action Required |
|---|---|---|
| 1-5 | Negligible | None |
| 6-20 | Minimal | Monitor, no immediate action |
| 21-50 | Noticeable on updates | Consider hoisting queries to parent |
| 51-100 | Significant overhead | Refactor: hoist queries or use select |
| 100+ | Critical impact | Immediate refactor: single query with props distribution |
| Pattern | Use Case | Example |
|---|---|---|
| useSuspenseQuery | Server Components integration, Suspense boundaries | |
| useQuery with enabled | Dependent queries, conditional fetching | |
| useQuery with select | Data transformation, subset selection | |
| useMutation optimistic | Low-latency UI updates, easily reversible | |
| useMutation pessimistic | High-stakes operations, server validation | |
| Symptom | Root Cause | Solution | Fallback if Solution Fails |
|---|---|---|---|
| Data doesn't update after save | Copied query data to useState | Use query data directly, derive with useMemo | Force refetch with refetch() method, check network tab for actual API response |
| Infinite requests | Unstable query keys (Date.now(), unsorted arrays) | Use deterministic key construction | Add staleness detection: |
| N duplicate requests | Query in every list item | Hoist query to parent, pass data as props | Ensure all components use identical queryKey (same object reference or values): |
| Query fires with undefined params | Missing enabled guard | Add | Use placeholderData to show loading state, add type guards in queryFn to throw early |
| Slow list rendering | N queries + N observers | Single parent query, distribute via props | Use select to subscribe to subset, implement virtual scrolling to reduce mounted components |
| Cache never clears | gcTime: Infinity on frequently-changing data | Match gcTime to data lifecycle | Force removal with queryClient.removeQueries(), monitor cache size with DevTools |
| UI shows stale data flash | Server cache stale, client cache fresh | Unified invalidation with same keys | Use initialData from server props, set refetchOnMount: false for hydrated queries |
| Optimistic update won't rollback | onError not restoring context | Use context from onMutate in onError | Force invalidation with invalidateQueries, implement manual rollback with previous state snapshot |
| Server hydration mismatch | Timestamp/user-specific data in SSR | Use suppressHydrationWarning on container | Client-only rendering with dynamic import and ssr: false, or normalize timestamps to UTC |
| Query never refetches | enabled: false guard blocking, or gcTime expired | Check enabled conditions, verify query isn't filtered by predicate | Increase gcTime to keep cache alive longer, use refetchInterval for polling behavior, check if staleTime: Infinity is preventing background refetches |
| Server action not invalidating | updateTag/revalidateTag using different keys than queryClient | Use same key factories for both server and client caches | Manually call router.refresh() after server action, verify tag names match query key hierarchy |
| Mutation succeeds but UI doesn't update | Missing onSuccess invalidation or wrong queryKey | Add | Use setQueryData to manually update cache: |
structuralSharing: falseretry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)initialData: serverData, refetchOnMount: false| Task Type | Freedom Level | Guidance Format | Example |
|---|---|---|---|
| Query configuration | High freedom | Principles with tables for common patterns | "Match staleTime to business requirements" |
| Optimistic updates | Medium freedom | Complete pattern with rollback handling | "Use onMutate/onError/onSettled callbacks" |
| QueryClient setup | Low freedom | Exact code with critical security warning | "NEVER use singleton on server - use factory" |