协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
accelint-tanstack-query-best-practices【开发与工程】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, invalidat…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Expert patterns for TanStack Query in modern React applications with Next.js App Router and Server Components.
setQueryData(key, (old) => ({ ...old, changed: value })) instead of setQueryData(key, newValue).This skill uses progressive disclosure to minimize context usage. Load references based on your scenario:
MANDATORY - READ ENTIRE FILE: Read query-client-setup.md (~125 lines) and server-integration.md (~151 lines) completely for server/client setup patterns.
Do NOT Load other references for initial setup.
Copy assets/query-client.ts for production-ready configuration.
query-keys.md (~151 lines) for key factory setupserver-integration.mdUse decision tables below for configuration values.
MANDATORY - READ ENTIRE FILE: Read mutations-and-updates.md (~345 lines) completely. Reference patterns-and-pitfalls.md for rollback patterns.
Do NOT Load caching-strategy.md for basic CRUD mutations.
patterns-and-pitfalls.mdfundamentals.md for structural sharingMANDATORY: Read caching-strategy.md (~198 lines) for unified Next.js use cache + TanStack Query + HTTP cache patterns.
Do NOT Load if only using client-side TanStack Query.
| 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 |
Use hierarchical factories for consistent invalidation:
// 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
Key stability rules:
| 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 |
Unified invalidation strategy:
| 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 |
Diagnosis:
| Pattern | Use Case | Example |
|---|---|---|
| useSuspenseQuery | Server Components integration, Suspense boundaries | useSuspenseQuery({ queryKey, queryFn }) |
| useQuery with enabled | Dependent queries, conditional fetching | useQuery({ queryKey, queryFn, enabled: !!userId }) |
| useQuery with select | Data transformation, subset selection | useQuery({ queryKey, queryFn, select: selectFn }) — extract selectFn to a stable module-level variable; inline functions re-run on every render |
| useMutation optimistic | Low-latency UI updates, easily reversible | useMutation({ onMutate, onError, onSettled }) |
| useMutation pessimistic | High-stakes operations, server validation | useMutation({ onSuccess }) |
| 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: const requestCount = useRef(0); useEffect(() => { requestCount.current++; if (requestCount.current > 10) console.error('Infinite loop detected', queryKey); }, [data]); See fundamentals.md for key stability patterns |
| 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): const queryKey = useMemo(() => keys.list(filters), [filters]); Increase staleTime to 30s to deduplicate rapid requests |
| Query fires with undefined params | Missing enabled guard | Add enabled: Boolean(dependency) | 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 onSuccess: () => queryClient.invalidateQueries({ queryKey }) | Use setQueryData to manually update cache: queryClient.setQueryData(keys.detail(id), newData), verify queryKey matches exactly |
Step 1: Check observer count in DevTools (use thresholds at lines 136-145)
Step 2: Check data size and update frequency
structuralSharing: false (see fundamentals.md for details)Step 3: Check React DevTools Profiler
Flaky connections:
retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)Token refresh needed:
Race conditions:
SSR mismatch (hydration error in console):
Client-server data drift:
initialData: serverData, refetchOnMount: falseHydrationBoundary not working:
Calibrate guidance specificity to mutation risk:
| 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" |
The test: "If the agent makes a mistake, what's the consequence?"
select only runs on successfully cached data — it is never called in error states; put validation and error throwing in queryFn