web-data-fetching-graphql-urql-f0cbc0 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-data-fetching-graphql-urql-f0cbc0 (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: URQL is a small core plus a pipeline of exchanges, and almost every configuration question is really a question about that pipeline's order — synchronous exchanges before asynchronous ones, error handlers before what they catch,fetchExchangelast. Caching is document-based by default, keyed on the query and its variables; normalized caching is an opt-in exchange. Hooks return a[result, execute]tuple, and the loading flag isfetching.
Detailed Resources:
useQuery, mutations, error handling, per-query contextpreferGetMethod, and the v4 → v6 migration stepsCombinedError shape, exchange cataloguecacheExchange from urql. A query plus its variables is onecache entry, and a mutation invalidates every entry whose result shared a __typename with it. Nothing to configure, and no way to edit the cache by hand.
cacheExchange from @urql/exchange-graphcache, replacing thedefault one. Entities are stored once by key, so keys, updates, resolvers and optimistic become available and mutations can edit the cache precisely. Adds roughly 8KB.
Start with the document cache. Move to Graphcache when a mutation needs to change a list the server did not return, or when you want optimistic updates.
<critical_requirements>
Order the exchanges: error handling, then synchronous, then asynchronous, with `fetchExchange` last. An operation passes through them in array order, so a cache placed after a network exchange never sees a request, and an error handler placed after authExchange never sees a failed refresh.
Put `__typename` in every optimistic response, along with every field a query reads. Graphcache normalizes on __typename plus the key, and a field the optimistic object omits is a field the watching query cannot render.
Set `preferGetMethod` to what the server accepts. From v6 the client sends queries under 2048 characters as GET; false forces POST for everything, and "force" sends GET regardless of length.
</critical_requirements>
Auto-detection: urql, @urql/core, @urql/exchange-graphcache, cacheExchange, fetchExchange, subscriptionExchange, mapExchange, ssrExchange, authExchange, retryExchange, useQuery, useMutation, useSubscription, requestPolicy, preferGetMethod, reexecuteQuery, CombinedError, wonka
Applies to:
Handled elsewhere:
mapExchange has caught them<philosophy>
The client itself does almost nothing: it turns a hook call into an operation and pushes it into a stream. Everything that looks like a feature — caching, auth, retries, deduplication, subscriptions, server rendering — is an exchange sitting in that stream, and every exchange sees the operation on the way out and the result on the way back.
Two things follow. Behaviour is added by installing an exchange rather than by configuring the client, so a project pays only for what it installs. And order is semantic rather than cosmetic: an exchange can only act on what has already reached it.
</philosophy>
<patterns>
import { Client, cacheExchange, fetchExchange } from "urql";
const client = new Client({
url: GRAPHQL_ENDPOINT,
exchanges: [cacheExchange, fetchExchange],
requestPolicy: "cache-first",
});<Provider value={client}> above the tree is what the hooks read; without it they throw at the first render rather than falling back to anything.
Full code: examples/core.md
const [result, reexecuteQuery] = useQuery<UsersData, UsersVariables>({
query: USERS_QUERY,
variables: { limit: DEFAULT_PAGE_SIZE },
requestPolicy: "cache-and-network",
});
const { data, fetching, error, stale } = result;
if (fetching && !data) return <Skeleton />;
if (error && !data) return <Error message={error.message} />;fetching is true for the first load and for every background refresh, so fetching && !data is what distinguishes them. stale marks cached data being revalidated — an "updating" hint rather than a spinner. pause: !userId holds a query back until its variables are real.
Default policy is cache-first; cache-and-network is the stale-while-revalidate one. The full table is in reference.md.
Full code: examples/core.md
const [result, executeMutation] = useMutation<CreatePostData>(CREATE_POST);
const response = await executeMutation({ input });
if (response.error) return;The execute function returns a promise carrying the result, so the error is handled at the call site rather than in a callback. result.fetching is what disables the form while it is in flight.
Full code: examples/core.md
exchanges: [
mapExchange, // errors, before anything whose failures it must see
cacheExchange, // synchronous, so it can answer without a request
authExchange, // headers, and refresh on a 401
retryExchange, // network failures only
fetchExchange, // always last
];Full code: examples/exchanges.md — auth with token refresh, retry configuration, TTL-based policy upgrades, and a custom exchange
import { cacheExchange } from "@urql/exchange-graphcache";
cacheExchange({
keys: { Product: (data) => data.sku as string },
updates: {
Mutation: {
createTodo: (result, _args, cache) =>
cache.updateQuery(/* add to the list */),
},
},
optimistic: {
toggleTodo: (args) => ({
__typename: "Todo",
id: args.id,
completed: args.completed,
}),
},
});Four keys, four jobs: keys says what identifies an entity, updates edits the cache after a mutation or a subscription event, resolvers invents fields on read, and optimistic writes a provisional entity into a separate layer that is discarded when the real result lands.
Full code: examples/exchanges.md
const [result] = useSubscription<NotificationData>({
query: NOTIFICATION_SUBSCRIPTION,
variables: { userId },
pause: !userId,
});Each event replaces data — accumulating a list takes the second argument, a handler that receives the previous value and the new event. Unsubscription happens on unmount without any cleanup.
Full code: examples/subscriptions.md
CombinedError carries both kinds at once, and they mean different things: networkError is a request that never completed, graphQLErrors is a response that arrived carrying failures.
if (error?.networkError) {
// nothing came back — offer a retry
}
if (error?.graphQLErrors.length) {
// some fields failed; `data` may still hold the rest
}
if (data && error) {
// render what arrived, with a warning
}A single if (error) branch throws away a page that mostly worked.
Full code: examples/core.md
const [result] = useQuery({
query: ADMIN_DATA_QUERY,
context: {
fetchOptions: {
headers: { "X-Admin-Token": process.env.ADMIN_TOKEN ?? "" },
},
url: process.env.ADMIN_GRAPHQL_URL ?? "",
requestPolicy: "network-only",
},
});context overrides the client's own settings for one operation — including the URL, which is how a second endpoint is reached without a second client.
Full code: examples/core.md
</patterns>
<red_flags>
Breaks at runtime:
Provider above them — they throw rather than degrading.fetchExchange before cacheExchange — every operation reaches the network and the cache isnever read.
mapExchange after authExchange — a failed token refresh passes it and reaches no handler.__typename — normalization fails silently and the UI does notmove.
data with no fetching or error branch — the first render has neither.preferGetMethod left at its default — short queries failand long ones succeed, which reads as an intermittent fault.
Surprising behaviour:
fetching covers background refreshes too, so a bare if (fetching) blanks the screen on everyrevalidation.
is two entries that never share anything.
id or _id — anything else needs a keys entry, and without onethe entity is not normalized at all.
entity.
retryIf should test networkError.
pollInterval option; TTL-based refresh comes from requestPolicyExchange.preferGetMethod: false being ignored — on 6.0.0 the opt-out does not take.dedupExchange was removed and deduplication moved into the core client; delete it from thearray rather than replacing it.
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.