skip to content

In urql, what do the four requestPolicy values do, and when would a storefront screen pick cache-and-network over the default?

level: middleimportance: must knowfreq 34%

answer

  1. four ways to use the cache
  2. default prefers what is stored
  3. show cached, refresh behind
  4. fetching false while stale true
  5. per client, per hook, per call

basics

~10 s

urql's requestPolicy is cache-first by default, returning a cached result or fetching once. cache-and-network returns the cached result marked stale and refetches, network-only always fetches, and cache-only never sends a request.

solid answer

~50 s

`requestPolicy` tells the cache exchange how to treat a query. `cache-first`, the default, returns a stored result and only sends a request on a miss. `cache-and-network` returns the stored result immediately with `stale: true` and also sends a request, so the screen updates when fresh data lands; with nothing cached it behaves like a normal fetch. `network-only` always sends a request, and the result still replaces the cached entry for later readers. `cache-only` never sends a request and yields an empty result on a miss. You set it on `createClient`, per `useQuery` call, or per re-run with `reexecuteQuery({ requestPolicy: 'network-only' })`. A storefront picks `cache-and-network` where data changes behind the user's back, such as stock levels on a product page: the page renders instantly from cache and corrects itself, and `stale` drives a subtle 'updating' hint because `fetching` stays `false`.

code

tsx · 19 lines
tsx
import { useQuery } from 'urql';

function StockBadge({ productId }: { productId: string }) {
  const [{ data, fetching, stale }, reexecuteQuery] = useQuery({
    query: ProductStockQuery,
    variables: { productId },
    requestPolicy: 'cache-and-network',
  });

  if (fetching) return <span>Checking stock…</span>;
  return (
    <span aria-busy={stale}>
      {data?.product.inStock ? 'In stock' : 'Sold out'}
      <button onClick={() => reexecuteQuery({ requestPolicy: 'network-only' })}>
        Refresh
      </button>
    </span>
  );
}

go deeper

for a junior

Know the four names and that cache-first is the default. Be able to say which one always hits the network and which one never does.

for a middle

Explain what each value does on a hit and on a miss, why network-only still fills the cache, and how fetching and stale differ under cache-and-network.

for a senior

Pick a policy per screen and justify it by how fast the data changes and what a stale value costs; mention requestPolicyExchange or refocusExchange for time-based refreshes.

for a principal

Treat policies as a product decision: agree which screens may show stale data and for how long, and keep invalidation separate from freshness instead of defaulting to network-only.

## What a request policy controls In urql every query becomes an **operation** that passes through the client's exchanges. The **request policy** is a field on that operation's context, and it tells the caching exchange whether it may answer from its store, must forward the operation to the network, or both. The default `cacheExchange` (the **document cache**, which stores one result per query-and-variables key) and Graphcache's normalized cache both read it. You can set it at three levels, each overriding the one before: 1. **On the client**: `createClient({ url, exchanges, requestPolicy: 'cache-and-network' })` changes the default for every query. 2. **On a hook**: `useQuery({ query, variables, requestPolicy: 'network-only' })` applies to that component's query. 3. **On a re-run**: `reexecuteQuery({ requestPolicy: 'network-only' })`, the second element `useQuery` returns, runs the query once more with a different policy, which is the usual way to build a refresh button. ## The four values | Policy | Result cached | Nothing cached | Sends a request? | |---|---|---|---| | `cache-first` (default) | returns it | fetches | only on a miss | | `cache-and-network` | returns it with `stale: true`, then the fresh one | fetches | always | | `network-only` | ignores it | fetches | always | | `cache-only` | returns it | returns an empty result | never | Two details are easy to miss: - A `network-only` result is still **written to the cache** on the way back. A later `cache-first` reader of the same query gets that fresh result without another request. - `cache-only` treats a miss as an empty result, not as an error, so the component renders with no `data` and no `error`. ## cache-and-network, fetching and stale `useQuery` exposes two flags that people conflate: - **`fetching`** is `true` only while the hook has no result for the current request yet, for instance on the very first load. - **`stale`** is `true` when the result on screen is known to be outdated or a newer request for it is in flight. Under `cache-and-network` with a cached result, the first render gets the cached data, `fetching: false` and `stale: true`. A spinner keyed to `fetching` therefore never shows, which is the point: the page is usable at once. When the network answer arrives, the hook re-renders with the fresh data and `stale: false`. If you want a small 'refreshing' indicator, key it to `stale`. ## Choosing per screen in a storefront - **Category and product lists** that change a few times a day suit `cache-first`: back-navigation is instant and costs no request. - **A product page showing stock or price** suits `cache-and-network`: the shopper sees the page immediately, and a sold-out size corrects itself within a round trip. - **The checkout summary** before payment suits `network-only`: a stale total there is a correctness problem, not a cosmetic one. - **An offline banner or a prefetched preview** can use `cache-only`, reading what earlier screens already loaded without triggering traffic. ## Refreshing without hand-picking policies Two optional exchanges upgrade policies for you, and both work by switching an operation to `cache-and-network`: - `requestPolicyExchange` from `@urql/exchange-request-policy` upgrades a `cache-first` query once its last network result is older than a `ttl`, five minutes by default, and accepts a `shouldUpgrade` function to limit which operations it touches. - `refocusExchange` from `@urql/exchange-refocus` re-runs active queries with `cache-and-network` when the page becomes visible again. They sit in the `exchanges` array before the cache, so the cache sees the upgraded policy. ## Common mistakes with request policies - **Expecting a spinner on refresh.** Under `cache-and-network` the refresh runs with `fetching: false`; a loading state keyed only to `fetching` never appears for it, by design. - **Reading never-loaded data with `cache-only`.** The screen renders blank, with neither `data` nor `error`, because a miss is not an error. - **Making `network-only` the client default.** Every screen then skips cache reads, so back-navigation always waits on the network. - **Treating `cache-and-network` as polling.** It refreshes once per request for the query, not on a timer; a live stock counter needs a subscription or a refresh trigger. ## Where the policy stops A request policy decides how one query uses the cache. It does not decide when cached results become wrong after a mutation; that is the cache exchange's invalidation logic, which differs between the document cache and Graphcache. Choosing `network-only` everywhere to avoid thinking about invalidation throws away the cache and doubles traffic on every navigation.

  • A developer switches every query to network-only to 'avoid stale data'. What does that cost?
    Every mount and every back-navigation now sends a request and shows a loading state, although results are still written to the cache. The cache is reduced to a write-only log. Stale data after a mutation is an invalidation problem; the document cache's `__typename` invalidation, `additionalTypenames` or Graphcache updaters address it without disabling reads.
  • How do you make urql refresh a product list in the background if it was last fetched more than a minute ago?
    Add `requestPolicyExchange({ ttl: 60_000 })` from `@urql/exchange-request-policy` before the cache exchange. When a query is re-requested after its `ttl` has passed, the exchange upgrades its policy to `cache-and-network`, so the cached list renders at once and a fresh one follows. The default `ttl` is five minutes.

saying these in an interview costs you the question

  • urql's default requestPolicy is cache-and-network, revalidating on every read.
  • network-only results bypass the cache and are never stored.
  • cache-and-network shows a loading spinner because fetching is true.
  • cache-only throws an error when nothing is cached.
  • requestPolicy can only be set once, on createClient.