web-data-fetching-swr-6635f0 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-data-fetching-swr-6635f0 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: SWR renders the cached value immediately and revalidates behind it, so the cache key is the whole identity of a request and an unstable key is the single most expensive mistake here.isLoadingcovers the first fetch only andisValidatingcovers every fetch, which is why using the second as a spinner hides the data SWR exists to show. Reads areuseSWR, writes areuseSWRMutation, and anullkey is how a request is skipped without breaking the rules of hooks.
Detailed Resources:
SWRConfiguseSWRMutation, optimistic updates, populateCache, global mutateuseSWRInfinite, infinite scroll, offset and filtered paginguseSWR, which fetches on mount and keeps the value fresh. Patterns 1–3.useSWRMutation, which does nothing until trigger() is called. Pattern 4.useSWRInfinite, whose getKey(pageIndex, previousPage) both builds eachpage's key and signals the end by returning null. Pattern 7.
suspense: true makes the component suspend and datanon-null; see examples/suspense.md. Everything else here still applies.
<critical_requirements>
Give each request a key that is stable across renders — a string, or an array of primitives. The key is the cache identity and the dependency: an object or array literal is a new reference every render, so SWR sees a new key, fetches again, re-renders, and repeats.
Throw from the fetcher on a non-OK response. SWR's error state is driven by a rejected promise, so a fetcher that returns res.json() unconditionally hands the error body over as data and no error branch ever runs.
Branch on `isLoading` for the first fetch and `isValidating` for a refresh in progress. isLoading is true only when there is no data yet, which is exactly when a skeleton is right; isValidating is true during background revalidation, when there is data on screen to keep.
Reach for `useSWRMutation` for anything that writes. useSWR fires on mount, so a POST written as a useSWR fetcher sends itself as soon as the component renders.
</critical_requirements>
Auto-detection: useSWR, useSWRMutation, useSWRInfinite, useSWRImmutable, SWRConfig, useSWRConfig, mutate, trigger, isValidating, revalidateOnFocus, dedupingInterval, keepPreviousData, fallbackData, optimisticData, rollbackOnError, populateCache, preload, swr/mutation, swr/infinite, swr/immutable
Applies to:
Handled elsewhere:
whole responses under a key
<philosophy>
The name is the algorithm: return what is cached, revalidate behind it, re-render if the answer changed. A component therefore has data at nearly every moment of its life, and the interesting states are not "loading or loaded" but "is this being checked" and "is this out of date".
Everything else follows. The key is a global identity, so two components asking for the same key share one request and one cache entry with no coordination between them. Revalidation is triggered by events the user causes — refocusing the tab, reconnecting — rather than by timers, because those are the moments the data on screen is most likely to be stale.
</philosophy>
<patterns>
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();
};Attaching status is what lets a component tell a 404 from a 500, and lets a retry policy decline to retry either. Define the fetcher at module scope — one created inside a component is a new reference on every render.
Full code: examples/core.md — client-based and multi-argument fetchers
// data: undefined, isLoading: true, isValidating: true — first fetch
// data: T, isLoading: false, isValidating: false — settled
// data: T, isLoading: false, isValidating: true — revalidating behind the value
// error: Error, isLoading: false, isValidating: false — failed with nothing cached
// data: T, error: Error, isLoading: false — failed with a cached value to showif (isLoading) return <Skeleton />;
return (
<div>
{isValidating && <RefreshIndicator />}
{error && data && <Banner>Showing cached data; the last refresh failed</Banner>}
<Content data={data} />
</div>
);The last row is the one to design for: a failed refresh over good data is a banner, not an error page.
Full code: examples/core.md
const ERROR_RETRY_COUNT = 3;
const DEDUP_INTERVAL_MS = 2000;
<SWRConfig
value={{
fetcher,
errorRetryCount: ERROR_RETRY_COUNT,
dedupingInterval: DEDUP_INTERVAL_MS,
keepPreviousData: true,
fallback, // keys pre-filled from a server render
}}
>
{children}
</SWRConfig>;A nested SWRConfig overrides its parent, which is how one static section opts out of revalidation without every hook in it repeating the options. Every option and its default is in reference.md.
Full code: examples/core.md
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 });The mutation fetcher's second parameter is { arg } — whatever trigger() was called with. trigger returns a promise, so the success path stays in the handler that called it.
Full code: examples/mutations.md
const { trigger } = useSWRMutation(`/api/todos/${todo.id}`, toggleTodo, {
optimisticData: (current: Todo) => ({
...current,
completed: !current.completed,
}),
rollbackOnError: true,
revalidate: true,
});optimisticData writes to the cache before the request leaves; rollbackOnError puts the previous value back when it fails. Where the response already contains the new state, populateCache writes it directly and revalidate: false skips the confirming round trip.
Full code: examples/mutations.md
// null key: the hook runs, the request does not
const { data } = useSWR(userId ? `/api/users/${userId}` : null, fetcher);
// dependent: the second key does not exist until the first resolves
const { data: user } = useSWR(`/api/users/${userId}`, fetcher);
const { data: posts } = useSWR(
user ? `/api/users/${user.id}/posts` : null,
fetcher,
);A key can also be a function returning null, which suits several conditions at once. What it must never be is a conditionally called hook.
Full code: examples/conditional.md
import useSWRInfinite from "swr/infinite";
const getKey = (pageIndex: number, previousPageData: PostsResponse | null) => {
if (previousPageData && !previousPageData.hasMore) return null; // stop
if (pageIndex === 0) return `/api/posts?limit=${PAGE_SIZE}`;
return `/api/posts?limit=${PAGE_SIZE}&cursor=${previousPageData?.nextCursor}`;
};
const { data, size, setSize } = useSWRInfinite<PostsResponse>(getKey, fetcher, {
revalidateFirstPage: false,
});
const posts = data?.flatMap((page) => page.posts) ?? [];data is an array of pages, so flatMap rather than map. getKey returning null is the only thing that ends the list — without it setSize keeps requesting.
Full code: examples/pagination.md
const POLL_INTERVAL_MS = 10 * 1000;
// live: poll, but not into a hidden tab
useSWR(key, fetcher, {
refreshInterval: POLL_INTERVAL_MS,
refreshWhenHidden: false,
});
// default: on focus and on reconnect, both already true
useSWR(key, fetcher);
// static: fetch once and leave it
import useSWRImmutable from "swr/immutable";
useSWRImmutable(key, fetcher);useSWRImmutable is the three revalidation options turned off, under one name.
Full code: examples/caching.md — plus preload() and cache persistence
</patterns>
<red_flags>
Breaks at runtime:
and the fetch loop never settles.
data, error stays undefined, andthe failure renders as content.
useSWR used for a write — it fires on mount, so the request is sent before any user acts.null key instead; the hook must run every render.suspense: true with no Suspense boundary above it — the thrown promise reaches the errorboundary or the top of the tree.
optimisticData without rollbackOnError — a failed write leaves the invented value in the cacheuntil something else revalidates.
Surprising behaviour:
null skips the request but undefined does not — it is interpolated, and the request goes to/api/users/undefined.
mutate() revalidates its own key; the global mutate() with no filter revalidates theentire cache.
revalidateOnFocus fires on every tab focus regardless of freshness — focusThrottleIntervalbounds it.
keepPreviousData: true on a search box shows the previous query's results under the new query.fallback keys are matched exactly, so /api/users/1 and /api/users/1/ hydrate separately.revalidateAll: true on useSWRInfinite refetches every loaded page on each revalidation.refreshInterval: 0 disables polling, which is what omitting it does.unless onErrorRetry says otherwise.
Error objects do not survive JSON serialization, so a persisted cache needs a structured errorshape.
aborted — a fetcher that must actually stop work in flight has to carry its own AbortController.
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.