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-expertdata-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.