dist/plugins/web-data-fetching-swr/skills/web-data-fetching-swr/SKILL.md
SWR data fetching patterns - useSWR, useSWRMutation, caching, revalidation, infinite scroll
npx skillsauth add agents-inc/skills web-data-fetching-swrInstall 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.
Quick Guide: SWR implements the stale-while-revalidate caching strategy: show cached data instantly, revalidate in the background. Keys must be stable (strings or stable arrays),
isLoadingis for initial fetches only (useisValidatingfor background refreshes), and all write operations go throughuseSWRMutation. The null key pattern is how you do conditional fetching -- never call hooks conditionally.
<critical_requirements>
(You MUST use a stable key -- keys should NOT change on every render or you'll trigger infinite requests)
(You MUST handle isLoading vs isValidating correctly -- isLoading is true only on initial fetch with no data)
(You MUST wrap mutations in useSWRMutation for write operations -- NOT useSWR)
(You MUST use named constants for ALL timeout, retry, and interval values -- NO magic numbers)
(You MUST use named exports only -- NO default exports)
</critical_requirements>
Auto-detection: SWR, useSWR, useSWRMutation, useSWRInfinite, useSWRImmutable, SWRConfig, mutate, revalidate, fetcher, stale-while-revalidate, preload
When to use:
When NOT to use:
Key patterns covered:
Detailed Resources:
SWR (stale-while-revalidate) returns cached data first, then revalidates in the background. This creates fast, responsive UIs while ensuring data freshness.
Core principles:
Trade-offs:
The fetcher must throw on non-OK responses. If it doesn't throw, SWR treats error bodies as valid data.
// lib/fetcher.ts
interface FetchError extends Error {
info: unknown;
status: number;
}
const fetcher = async <T>(url: string): Promise<T> => {
const response = await fetch(url);
if (!response.ok) {
const error = new Error("Fetch failed") as FetchError;
error.info = await response.json().catch(() => null);
error.status = response.status;
throw error;
}
return response.json();
};
export { fetcher };
export type { FetchError };
Why good: Throws on error (required for SWR error state to work), attaches status for conditional handling, typed error enables downstream type narrowing
See examples/core.md for axios, GraphQL, and multi-argument fetcher variants.
The most common SWR mistake. isLoading is true only on initial fetch with no data. isValidating is true during any in-flight request.
// State combinations:
// Initial load: { data: undefined, isLoading: true, isValidating: true }
// Success: { data: T, isLoading: false, isValidating: false }
// Revalidating: { data: T, isLoading: false, isValidating: true }
// Error (no data): { error: Error, isLoading: false, isValidating: false }
// Error (has data): { data: T, error: Error, isLoading: false }
// BAD: Using isValidating as loading indicator hides cached data
if (isValidating) return <Spinner />;
// GOOD: isLoading for initial, isValidating for refresh indicator
if (isLoading) return <Spinner />;
return (
<div>
{isValidating && <RefreshIndicator />}
{error && data && <Banner>Data may be outdated</Banner>}
<Content data={data} />
</div>
);
Why bad: Showing spinner during background revalidation hides perfectly valid cached data, defeating the purpose of stale-while-revalidate
See examples/core.md for full state handling with error + stale data combinations.
Centralize fetcher, retry, and revalidation settings. Nested SWRConfig overrides parent config.
const ERROR_RETRY_COUNT = 3;
const ERROR_RETRY_INTERVAL_MS = 5000;
const DEDUP_INTERVAL_MS = 2000;
<SWRConfig value={{
fetcher,
errorRetryCount: ERROR_RETRY_COUNT,
errorRetryInterval: ERROR_RETRY_INTERVAL_MS,
dedupingInterval: DEDUP_INTERVAL_MS,
keepPreviousData: true,
fallback, // Pre-fetched data for SSR hydration
}}>
{children}
</SWRConfig>
Why good: Eliminates config duplication across components, fallback prop enables SSR data hydration, nested configs allow per-section overrides
See examples/core.md for full provider setup and nested config override patterns.
Never use useSWR for mutations. useSWR fires on mount -- useSWRMutation fires on demand via trigger().
import useSWRMutation from "swr/mutation";
async function createPost(
url: string,
{ arg }: { arg: CreatePostInput },
): Promise<Post> {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg),
});
if (!response.ok) throw new Error("Failed to create post");
return response.json();
}
const { trigger, isMutating, error, reset } = useSWRMutation(
"/api/posts",
createPost,
);
await trigger({ title, content });
Why good: trigger() gives explicit control over when mutation fires, isMutating provides loading state, reset clears error state, separate from useSWR keeps read/write concerns apart
See examples/mutations.md for optimistic updates, cache invalidation, and populateCache patterns.
Update UI immediately while mutation is in-flight. Rollback on error.
const { trigger } = useSWRMutation(`/api/todos/${todo.id}`, toggleTodo, {
optimisticData: (currentData: Todo) => ({
...currentData,
completed: !currentData.completed,
}),
rollbackOnError: true,
revalidate: true,
});
Why good: optimisticData shows instant feedback, rollbackOnError ensures consistency on failure, revalidate: true syncs with server after success
See examples/mutations.md for list-level optimistic updates and populateCache for skipping revalidation.
Pass null as the key to skip the request. Never call hooks conditionally.
// BAD: Conditional hook call (breaks Rules of Hooks)
if (!userId) return <SelectUser />;
const { data } = useSWR(`/api/users/${userId}`, fetcher);
// GOOD: Null key prevents request without conditional hook
const { data } = useSWR(userId ? `/api/users/${userId}` : null, fetcher);
// GOOD: Dependent queries -- second waits for first
const { data: user } = useSWR(`/api/users/${userId}`, fetcher);
const { data: posts } = useSWR(user ? `/api/users/${user.id}/posts` : null, fetcher);
Why good: Hook always called (no Rules of Hooks violation), null key is idiomatic SWR pattern, enables data cascades for dependent queries
See examples/conditional.md for auth-gated, feature-flag, and complex multi-condition patterns.
The getKey function receives page index and previous page data. Return null to stop.
import useSWRInfinite from "swr/infinite";
const PAGE_SIZE = 20;
const getKey = (pageIndex: number, previousPageData: PostsResponse | null) => {
if (previousPageData && !previousPageData.hasMore) return null; // End
if (pageIndex === 0) return `/api/posts?limit=${PAGE_SIZE}`;
return `/api/posts?limit=${PAGE_SIZE}&cursor=${previousPageData?.nextCursor}`;
};
const { data, size, setSize, isLoading } = useSWRInfinite<PostsResponse>(
getKey,
fetcher,
{
revalidateFirstPage: false,
},
);
const posts = data?.flatMap((page) => page.posts) ?? [];
const isReachingEnd = data?.[data.length - 1]?.hasMore === false;
Why good: getKey returning null stops fetching, flatMap flattens pages, revalidateFirstPage: false prevents refetching all pages on focus
See examples/pagination.md for IntersectionObserver infinite scroll, offset pagination, and filtered pagination with reset.
Choose strategy based on data freshness requirements.
const POLL_INTERVAL_MS = 10 * 1000;
// Real-time: polling
useSWR(key, fetcher, {
refreshInterval: POLL_INTERVAL_MS,
refreshWhenHidden: false,
});
// Default: revalidate on focus/reconnect (enabled by default)
useSWR(key, fetcher, { revalidateOnFocus: true, revalidateOnReconnect: true });
// Static: disable all revalidation
useSWR(key, fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
});
// Shorthand for static: useSWRImmutable
import useSWRImmutable from "swr/immutable";
useSWRImmutable(key, fetcher);
Why good: Different strategies for different freshness needs, useSWRImmutable is cleaner than disabling all options manually, refreshWhenHidden: false prevents polling when tab is hidden
See examples/caching.md for prefetching with preload(), cache persistence with localStorage, and deduplication.
<red_flags>
High Priority Issues:
isLoading for initial load only.useSWR fires on mount. Use useSWRMutation for POST/PUT/DELETE.data, error state never triggers.if (!userId) return; const { data } = useSWR(...) breaks Rules of Hooks. Use null key pattern.Medium Priority Issues:
rollbackOnError with optimisticData -- Without rollback, failed mutations leave stale optimistic data in cache.keepPreviousData: true for search -- Shows stale search results for a different query. Set to false for search.revalidateAll: true with useSWRInfinite -- Refetches all loaded pages on every focus event. Disable for performance.Gotchas & Edge Cases:
null key stops fetching, but undefined key still fetches (gets coerced to string "undefined")mutate() without arguments revalidates the bound key only, but global mutate() without a key filter revalidates everythingrefreshInterval: 0 disables polling (same as omitting the option)revalidateOnFocus fires on every tab focus even if data is fresh (use focusThrottleInterval to limit)useSWR with same key share cache and deduplicate requests automaticallyfallback in SWRConfig must match exact key strings -- /api/users/1 and /api/users/1/ are different keysuseSWRInfinite revalidates all pages by default (set revalidateAll: false)useSWRImmutable in v2.4+ properly overrides global refreshInterval settings (fixed from earlier versions)</red_flags>
<critical_reminders>
(You MUST use a stable key -- keys should NOT change on every render or you'll trigger infinite requests)
(You MUST handle isLoading vs isValidating correctly -- isLoading is true only on initial fetch with no data)
(You MUST wrap mutations in useSWRMutation for write operations -- NOT useSWR)
(You MUST use named constants for ALL timeout, retry, and interval values -- NO magic numbers)
(You MUST use named exports only -- NO default exports)
Failure to follow these rules will cause infinite request loops, incorrect loading states, and unmaintainable code.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety