skip to content

With graphql_flutter, which FetchPolicy does a Query widget use by default, and when would you choose each of the other policies?

level: middleimportance: must knowfreq 48%

answer

  1. widget watches, client.query does not
  2. Query widget: cacheAndNetwork
  3. client.query: cacheFirst
  4. networkOnly saves, noCache does not
  5. DefaultPolicies on the client

basics

~20 s

A Query widget watches its query, so it uses the watchQuery default, FetchPolicy.cacheAndNetwork; a one-shot client.query defaults to cacheFirst, and mutations and subscriptions to networkOnly. Override per operation in the options or per client with DefaultPolicies.

solid answer

~40 s

The `Query` widget and `useQuery` hook turn their options into a watched query, so they take `client.defaultPolicies.watchQuery`, whose fetch policy is `FetchPolicy.cacheAndNetwork`: cached data renders at once and the network result follows. A one-shot `client.query` defaults to `cacheFirst`, which returns cached data and never refreshes it while it is there. Mutations and subscriptions default to `networkOnly`. I pick `cacheFirst` for data that rarely changes, `networkOnly` when the screen must show server truth but should still refresh the cache, `noCache` when the result should not touch the cache at all, and `cacheOnly` for offline reads that fail rather than hit the network. The policy goes in `QueryOptions(fetchPolicy: ...)`, or app-wide through `GraphQLClient(defaultPolicies: DefaultPolicies(...))`.

code

dart · 20 lines
dart
final bookDetails = gql(r'''
  query Book($id: ID!) { book(id: $id) { id title author coverUrl } }
''');

Widget build(BuildContext context) {
  return Query(
    options: QueryOptions(
      document: bookDetails,
      variables: {'id': bookId},
      fetchPolicy: FetchPolicy.cacheFirst,
    ),
    builder: (result, {refetch, fetchMore}) {
      if (result.hasException) return Text(result.exception.toString());
      if (result.isLoading && result.data == null) {
        return const CircularProgressIndicator();
      }
      return BookHeader(data: result.data!['book'] as Map<String, dynamic>);
    },
  );
}

go deeper

for a junior

Recall the five FetchPolicy values and that a Query widget shows cached data first and then refreshes from the network.

for a middle

Explain why the Query widget defaults to cacheAndNetwork while client.query defaults to cacheFirst, and how to override per operation or per client.

for a senior

Trace stale-screen reports to the policy in use, and choose policies per screen by how fresh the data must be and what it costs to fetch.

for a principal

Set freshness expectations per data type with product and backend teams, so client policies and server caching agree.

## What a fetch policy decides A **`FetchPolicy`** tells `graphql_flutter` (through the `graphql` package) where a result may come from and whether it is written back to the cache. There are five values: | Policy | Reads cache | Goes to network | Writes result to cache | |---|---|---|---| | `cacheFirst` | yes, and stops there if data is found | only on a cache miss | yes | | `cacheAndNetwork` | yes, emits it first | always | yes | | `networkOnly` | no | always; fails if the call fails | yes | | `noCache` | no | always; fails if the call fails | **no** | | `cacheOnly` | yes | never; fails if data is missing | not applicable | ## The defaults, and why the widget differs The defaults live in `DefaultPolicies` on the client and depend on **how** the operation runs: - **`watchQuery`** (used by the `Query` widget and the `useQuery` hook): **`cacheAndNetwork`**; - **`query`** (a one-shot `client.query(...)` call): **`cacheFirst`**; - **`mutate`** and **`watchMutation`** (the `Mutation` widget): **`networkOnly`**; - **`subscribe`**: **`networkOnly`**; `cacheOnly` is invalid for subscriptions. The `Query` widget converts its `QueryOptions` into watch-query options and fills any unset policy from `defaultPolicies.watchQuery`. That is why the same query document behaves differently in a widget and in a repository calling `client.query`: the widget always refreshes, the one-shot call is satisfied by whatever is cached. ## Choosing a policy in a book-club app 1. **Book details** (title, author, cover) change rarely: `cacheFirst` avoids a request each time the page opens. 2. **Review list** on a book page: the default `cacheAndNetwork` shows cached reviews instantly and then the fresh list, so members see new reviews without waiting on a spinner. 3. **Pull-to-refresh**: call the `refetch` function the `Query` builder receives; to guarantee a server round trip from a repository, use `networkOnly`. 4. **A one-off moderation report** that should not linger on the device: `noCache`. 5. **Offline mode**: `cacheOnly` renders what was persisted and fails fast instead of waiting on a dead network. ## Stale reads explained by policy Most "the screen shows old data" reports trace back to one of two causes: - a repository using **`client.query`** with its default `cacheFirst`, so once a result is cached it is served forever; either choose `networkOnly` there or refetch explicitly; - a **`noCache`** query whose result never reaches the cache, so other widgets watching the same data are not updated by it. ## Setting policies - **Per operation**: `QueryOptions(document: ..., fetchPolicy: FetchPolicy.cacheFirst)`; also `errorPolicy` and `cacheRereadPolicy` in the same options. - **Per client**: `GraphQLClient(link: ..., cache: ..., defaultPolicies: DefaultPolicies(query: Policies(fetch: FetchPolicy.networkOnly)))`. Unset fields keep their built-in defaults, because each `Policies` value is merged over them. - **Polling**: `QueryOptions(pollInterval: ...)` re-runs a watched query on a timer, useful for a list that should refresh while visible. ## Error policy sits next to fetch policy The same options carry an **`errorPolicy`**, which decides what happens when the server returns GraphQL errors alongside data: - **`ErrorPolicy.none`** (the default for every operation type): errors are treated like network errors and any returned data is ignored; - **`ErrorPolicy.ignore`**: the data is used and the errors are not reported to the UI; - **`ErrorPolicy.all`**: both data and errors are kept, so a screen can show what it has and a warning. For a review list where one review's author failed to resolve, `all` lets the page render the other reviews instead of an error screen. Choosing it means the builder must check `result.hasException` and `result.data` independently. What a normalised cache is and why partial reads miss are GraphQL-client concepts shared across ecosystems; the Flutter-specific part is which default each API picks and where to override it.

  • What is the difference between networkOnly and noCache?
    Both always go to the network and fail if the request fails. `networkOnly` writes the result into the cache, so other watchers of the same data update; `noCache` leaves the cache untouched, so the result is visible only to the caller.
  • How do you change the default for every Query widget in the app?
    Pass `defaultPolicies: DefaultPolicies(watchQuery: Policies(fetch: ...))` to the `GraphQLClient`. The `Query` widget and `useQuery` hook fill unset options from `defaultPolicies.watchQuery`, and fields you leave out keep their built-in values.

saying these in an interview costs you the question

  • A Query widget and client.query share the same cacheFirst default
  • cacheFirst refreshes cached data in the background
  • noCache still writes the network result into the cache
  • Mutations read from the cache by default before hitting the network
  • cacheOnly falls back to the network when the cache is empty