
Tanstack Query Best Practices
Tune TanStack Query gcTime and defaults so client caches retain useful data without memory leaks or zero-cache mistakes.
Overview
tanstack-query-best-practices is an agent skill for the Build phase that teaches how to set TanStack Query gcTime and defaults for inactive cache retention without leaks or useless zero-cache configs.
Install
npx skills add https://github.com/deckardger/tanstack-agent-skills --skill tanstack-query-best-practicesWhat is this skill?
- CRITICAL-priority rule: configure gcTime (formerly cacheTime) for inactive query retention
- Documents default 5-minute gcTime and tradeoffs of Infinity vs gcTime: 0
- Good examples: 30 minutes for dashboard stats, 2 minutes for large one-off reports
- Bad examples: never GC, immediate GC, and ignoring revisit patterns
- Default inactive query gcTime: 5 minutes
- Example tuning: 30-minute gcTime for dashboard stats; 2-minute gcTime for large reports
Adoption & trust: 6.9k installs on skills.sh; 180 GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
Your app refetches too often or holds too much in memory because gcTime was left default, set to Infinity, or zeroed out.
Who is it for?
Builders shipping data-heavy dashboards or multi-page React apps with TanStack Query who need explicit cache retention policy.
Skip if: Projects without client-side async data fetching or teams that only need staleTime tuning with no cache lifecycle concerns.
When should I use this skill?
Configuring TanStack Query gcTime, defaultOptions, or diagnosing cache drops and memory growth from poor retention settings.
What do I get? / Deliverables
Queries get navigation-aware gcTime values and sensible QueryClient defaults that balance freshness, memory, and instant revisits.
- Per-query and global gcTime recommendations
- Documented bad vs good configuration examples
Recommended Skills
Journey fit
Build is the canonical phase for configuring QueryClient and per-query cache behavior in app code. Frontend subphase covers useQuery options, QueryClient defaultOptions, and navigation-aware retention for dashboard and report views.
How it compares
Focused gcTime cache-lifecycle rules, not a full TanStack Query migration or server-component guide.
Common Questions / FAQ
Who is tanstack-query-best-practices for?
Solo frontend devs using agents to configure TanStack Query caches in SaaS UIs and internal tools.
When should I use tanstack-query-best-practices?
During Build when defining QueryClient defaults or per-query options for dashboards, reports, and other revisit-heavy routes.
Is tanstack-query-best-practices safe to install?
It is configuration guidance only; review the Security Audits panel on this page before installing skills into your agent environment.
SKILL.md
READMESKILL.md - Tanstack Query Best Practices
# cache-gc-time: Configure gcTime for Inactive Query Retention ## Priority: CRITICAL ## Explanation `gcTime` (garbage collection time, formerly `cacheTime`) controls how long inactive queries remain in the cache before being garbage collected. Default is 5 minutes. Configure based on your navigation patterns and memory constraints. ## Bad Example ```tsx // Not considering gcTime for frequently revisited pages const { data } = useQuery({ queryKey: ['dashboard-stats'], queryFn: fetchDashboardStats, // Default gcTime of 5 minutes - might be too short for frequently revisited data }) // Setting gcTime too high without consideration const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: Infinity, // Never garbage collect - potential memory leak }, }, }) // Setting gcTime to 0 - cache is immediately removed const { data } = useQuery({ queryKey: ['user-data'], queryFn: fetchUserData, gcTime: 0, // Loses cache benefits entirely }) ``` ## Good Example ```tsx // Longer gcTime for frequently revisited data const { data } = useQuery({ queryKey: ['dashboard-stats'], queryFn: fetchDashboardStats, gcTime: 30 * 60 * 1000, // 30 minutes - user returns to dashboard often }) // Shorter gcTime for rarely revisited large data const { data: report } = useQuery({ queryKey: ['detailed-report', reportId], queryFn: () => fetchReport(reportId), gcTime: 2 * 60 * 1000, // 2 minutes - large payload, viewed once }) // Sensible default with query-specific overrides const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 10 * 60 * 1000, // 10 minutes default }, }, }) ``` ## Understanding gcTime vs staleTime ``` Query Mount → Data Fresh (staleTime) → Data Stale → Query Unmount → gcTime countdown → Garbage Collected Timeline example (staleTime: 1min, gcTime: 5min): 0:00 - Query mounts, fetches data 0:00-1:00 - Data is fresh (no background refetch) 1:00+ - Data is stale (background refetch on new mount) 5:00 - User navigates away, query unmounts 5:00-10:00 - Data in cache but inactive (gcTime countdown) 10:00 - Data garbage collected (next mount = full loading state) ``` ## Recommended gcTime Values | Scenario | gcTime | Rationale | |----------|--------|-----------| | Frequently revisited routes | 15 - 30min | Instant navigation | | Detail pages (viewed once) | 2 - 5min | Memory efficient | | Large payloads | 1 - 2min | Prevent memory bloat | | Critical user data | 30min+ | Offline-like experience | | SSR hydration | >= 2s | Prevent hydration issues | ## Context - gcTime countdown starts when ALL query observers unmount - Remounting before gcTime expires returns cached data instantly - Setting gcTime < staleTime is rarely useful - For SSR, avoid gcTime: 0 (use minimum 2000ms to allow hydration) - Monitor memory usage in long-running applications # cache-invalidation: Use Targeted Invalidation Over Broad Patterns ## Priority: CRITICAL ## Explanation Query invalidation marks cached data as stale, triggering background refetches. Use targeted invalidation to refresh only affected data. Overly broad invalidation causes unnecessary network requests; too narrow invalidation leaves stale data. ## Bad Example ```tsx // Invalidating everything after a single todo update const mutation = useMutation({ mutationFn: updateTodo, onSuccess: () => { queryClient.invalidateQueries() // Invalidates ENTIRE cache }, }) // Invalidating too broadly const mutation = useMutation({ mutationFn: updateTodoStatus, onSuccess: () => { // Invalidates all todos including unrelated lists queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) // Missing invalidation of related queries const mutation = useMutation({ mutationFn: addComment, onSuccess: () => { // Only invalidates comment list, misses comment count queryClient.invalidateQueries({ queryKey: ['comments', postId] }) }, }) ``` ## Good Example ```tsx // Targeted i