skills/data-fetching-strategist/SKILL.md
Architect data fetching with TanStack Query v5, SWR, optimistic updates, prefetching, and cache invalidation strategies. Activate on: React Query, SWR, optimistic updates, cache invalidation, prefetching, stale-while-revalidate, infinite queries. NOT for: REST API design (use api-architect), database queries (use database skills), GraphQL schema (use graphql-expert).
npx skillsauth add curiositech/windags-skills data-fetching-strategistInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Data Complexity?
├─ Simple GET requests, minimal caching needs
│ └─ Use SWR: lighter bundle, simpler API
├─ Complex mutations, optimistic updates, prefetching
│ └─ Use TanStack Query v5: full-featured
└─ Static data, build-time fetching
└─ Use manual fetch in RSC/getStaticProps
Data Freshness Requirements?
├─ Real-time (user notifications, live chat)
│ ├─ staleTime: 0, refetchInterval: 5000
│ └─ Consider WebSocket instead
├─ Frequent updates (user profiles, dashboards)
│ └─ staleTime: 30000 (30s), gcTime: 300000 (5m)
├─ Occasional updates (product catalogs, articles)
│ └─ staleTime: 300000 (5m), gcTime: 600000 (10m)
└─ Static data (countries, categories)
└─ staleTime: Infinity, gcTime: Infinity
Mutation Failed?
├─ Network error (offline, timeout)
│ ├─ Show "offline" indicator
│ ├─ Queue mutation for retry when online
│ └─ Keep optimistic update visible
├─ Validation error (400, 422)
│ ├─ Rollback optimistic update immediately
│ ├─ Show field-level errors
│ └─ Focus first error field
├─ Authorization error (401, 403)
│ ├─ Rollback optimistic update
│ ├─ Clear auth tokens
│ └─ Redirect to login
└─ Server error (500, 503)
├─ Rollback optimistic update
├─ Show generic error message
└─ Auto-retry up to 3 times
Invalidation Scope Needed?
├─ Invalidate all related data (user updates profile)
│ └─ Hierarchical: ['users', userId, 'profile']
│ └─ Allows: invalidateQueries(['users', userId])
├─ Invalidate specific subset (filter changes)
│ └─ Include filters: ['products', 'list', { category, sort }]
│ └─ Allows: invalidateQueries(['products', 'list'])
└─ Never invalidate together
└─ Separate top-level: ['analytics'], ['notifications']
Navigation Pattern?
├─ Mouse-driven (hover to preview)
│ └─ prefetchQuery on onMouseEnter + onFocus
├─ Swipe/touch interface
│ └─ prefetchQuery on visible list items (intersection observer)
├─ Predictable flow (multi-step form)
│ └─ prefetchQuery on current step completion
└─ Route-based preloading
└─ prefetchQuery in route loader/middleware
Symptoms: UI shows wrong data after rapid clicks, mutations overwrite each other Detection: User clicks fast → sees data flicker → final state doesn't match last action Fix:
// Cancel in-flight queries before optimistic update
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey });
const previous = queryClient.getQueryData(queryKey);
queryClient.setQueryData(queryKey, optimisticUpdate);
return { previous };
}
Symptoms: User sees old data mixed with new, inconsistent states across components Detection: Related data shows different timestamps, user reports "data doesn't match" Fix: Invalidate query hierarchies, not individual queries:
// Wrong: only invalidates specific filter
invalidateQueries(['products', 'list', filters]);
// Right: invalidates all product lists
invalidateQueries(['products', 'list']);
Symptoms: Browser memory grows continuously, tabs become unresponsive Detection: DevTools shows growing query cache, gcTime set too high or Infinity everywhere Fix: Configure appropriate garbage collection:
gcTime: 5 * 60 * 1000, // 5 minutes, not Infinity
Symptoms: Hundreds of identical requests, poor performance, API rate limits hit Detection: Network tab shows duplicate requests, staleTime: 0 in all queries Fix: Set appropriate staleness thresholds:
staleTime: 30 * 1000, // 30 seconds minimum
Symptoms: UI shows success but data never actually saved, users lose work Detection: Optimistic update stays after page refresh, server never received mutation Fix: Always implement rollback and settled states:
onError: (error, variables, context) => {
queryClient.setQueryData(queryKey, context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey });
}
Scenario: E-commerce product page with review system requiring optimistic updates and prefetching.
Step 1: Analyze requirements
Step 2: Design query key hierarchy
const queryKeys = {
products: {
all: ['products'],
detail: (id: string) => ['products', 'detail', id],
reviews: (productId: string) => ['products', productId, 'reviews'],
}
};
Step 3: Configure queries with appropriate staleness
// Product query - data changes infrequently
const { data: product } = useQuery({
queryKey: queryKeys.products.detail(productId),
queryFn: () => fetchProduct(productId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
// Reviews query - more dynamic
const { data: reviews } = useQuery({
queryKey: queryKeys.products.reviews(productId),
queryFn: () => fetchReviews(productId),
staleTime: 30 * 1000, // 30 seconds
});
Step 4: Implement optimistic review addition
const addReviewMutation = useMutation({
mutationFn: (review: NewReview) =>
fetch(`/api/products/${productId}/reviews`, {
method: 'POST',
body: JSON.stringify(review)
}),
onMutate: async (newReview) => {
await queryClient.cancelQueries({
queryKey: queryKeys.products.reviews(productId)
});
const previousReviews = queryClient.getQueryData(
queryKeys.products.reviews(productId)
);
// Add optimistic review with pending ID
const optimisticReview = {
id: `temp-${Date.now()}`,
...newReview,
createdAt: new Date().toISOString()
};
queryClient.setQueryData(
queryKeys.products.reviews(productId),
(old: Review[]) => [optimisticReview, ...old]
);
return { previousReviews };
},
onError: (error, newReview, context) => {
queryClient.setQueryData(
queryKeys.products.reviews(productId),
context.previousReviews
);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.products.reviews(productId)
});
}
});
Step 5: Add prefetching for related products
const PrefetchLink = ({ productId, children }) => {
const queryClient = useQueryClient();
const prefetchProduct = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.products.detail(productId),
queryFn: () => fetchProduct(productId),
staleTime: 30_000,
});
};
return (
<Link
href={`/products/${productId}`}
onMouseEnter={prefetchProduct}
onFocus={prefetchProduct}
>
{children}
</Link>
);
};
Expert catches vs Novice misses:
Do NOT use this skill for:
api-architect insteadgraphql-expert insteadreact-server-components-expertwebsocket-integration-expertreact-state-management insteadDelegation rules:
api-architectreact-server-components-expertwebsocket-integration-experttools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.