In Apollo Client 4, how do you choose between refetchQueries, an update function and the mutation payload to show a newly created issue?
answer
- entities versus the lists that hold them
- the payload fixes fields, not membership
- update runs after the result is written
- refetching costs a round trip
- awaitRefetchQueries changes what mutate waits for
basics
~20 sIn Apollo Client 4 a mutation's normalized payload updates issues already cached but never adds one to a list. Use an update function to insert the new issue without a request, or refetchQueries when the server must decide membership or order.
solid answer
~50 sApollo normalizes each object in a mutation result that carries an id under its cache ID, so `closeIssue` returning `{ id status }` repaints that row with no extra code. A new issue is different. `createIssue` returns an `Issue:57` entity, but the cached `issues` field is a list of references that Apollo will not edit on its own. Two `useMutation` options close the gap. An `update(cache, { data })` function runs after the result is written and can append a reference with `cache.modify`. The list changes instantly with no request, but you are re-implementing the server's filter and sort rules. `refetchQueries: ["Issues"]` (a name, a document, or `"active"`) re-runs watched queries so the server decides, at the cost of a round trip. Add `awaitRefetchQueries: true` if the `mutate` promise should wait for that refetch. Choose `update` for simple lists and a refetch for filtered or sorted ones.
code
tsx · 26 linesimport { gql, type TypedDocumentNode } from "@apollo/client";
import { useMutation } from "@apollo/client/react";
type Issue = { __typename: "Issue"; id: string; title: string; status: "OPEN" | "CLOSED" };
const CREATE_ISSUE: TypedDocumentNode<{ createIssue: Issue }, { title: string }> = gql`
mutation CreateIssue($title: String!) {
createIssue(title: $title) { id title status }
}
`;
export function useCreateIssue() {
return useMutation(CREATE_ISSUE, {
update(cache, { data }) {
if (!data) return;
cache.modify({
fields: {
issues(existing = [], { toReference }) {
const ref = toReference(data.createIssue);
return ref ? [...existing, ref] : existing;
},
},
});
},
});
}go deeper
Know that returning the changed fields from a mutation updates issues already on screen, and that a new issue needs refetchQueries or an update function to reach a list.
Explain the trade-off: update is instant but re-implements list rules, refetchQueries is correct but costs a round trip. Describe what awaitRefetchQueries changes about the mutate promise.
Pick per mutation, combine update with refetchQueries or onQueryUpdated when the list is server-computed, and know that only queries with subscribers are refetched.
Set a team convention for keeping lists correct after writes, deciding which lists may be patched locally and which must always come from the server, and how reviews catch drift.
## Three ways the list can change After a mutation, Apollo Client 4 can bring the screen up to date in three ways. Each one has a different cost and hands a different party the job of deciding what the list contains. | Approach | Extra request | Who decides list membership | Good for | |---|---|---|---| | Rely on the mutation payload | none | nobody, since lists are untouched | edits to issues already on screen | | `update` function | none | your client code | simple lists with obvious order | | `refetchQueries` | one per refetched query | the server | filtered, sorted or computed lists | The choice matters in an interview because each answer is right for a different mutation in the same issue tracker. ## What the payload alone does When a mutation completes with the default mutation `fetchPolicy` (`network-only`), Apollo writes its result into the normalized cache. Every object that carries `__typename` and `id` is merged into its entity record, such as `Issue:42`. - `closeIssue(id: 42)` returning `{ id status }` overwrites `status` on `Issue:42`. Every query reading that issue re-renders with the new status, and no other code is needed. - `createIssue` returning `{ id title status }` creates `Issue:57`. That record now exists, but no cached list points at it yet. - The cached `issues` root field holds references like `{ __ref: "Issue:42" }` and is only rewritten by a query result or by your code. So the payload handles **field changes** to known entities and never handles **membership changes**. The reason lists are not repaired automatically is a protocol-level question about normalized caches. The Apollo question is which API you reach for. ## Writing the update function `update` is a `useMutation` option (it can also be passed to `mutate`) with the signature `update(cache, result, { context, variables })`. For `createIssue` it runs like this: 1. The server responds, and Apollo writes `Issue:57` into the cache. 2. Apollo calls `update(cache, { data })` with the same result. 3. Your code calls `cache.modify` on the `issues` field and appends a reference to the new entity, obtained with the modifier's `toReference` helper. 4. Apollo broadcasts the change, and every query reading `issues` re-renders with the new row. Because the edit happens locally, the new issue shows up with no round trip. The price is that your code now encodes the list's rules. Appending works for a list in creation order. A list sorted by priority, filtered by assignee or capped at 50 needs the same logic the server applies, and getting it wrong shows the user a list the server would never return. The `cache.modify`, `readQuery` and `writeQuery` calls themselves belong to the cache API. The mutation option that hosts them is the `update` function. ## refetchQueries and awaitRefetchQueries `refetchQueries` asks Apollo to re-run queries once the mutation result is in: - An array of operation names (`"Issues"`) or query documents refetches those queries with their current variables. - The shorthand `"active"` refetches every active query, and `"all"` also includes inactive ones such as skipped or `standby` queries. - It can also be a function of the mutation result that returns such a list. - Only queries with at least one subscriber can be refetched. A name that matches no mounted query logs a warning in development and does nothing. By default the refetch runs in the background: the `mutate` promise resolves once the mutation result is processed, and the list updates a moment later. **`awaitRefetchQueries: true`** makes the `mutate` promise, and the hook's `loading`, wait until the refetched queries finish. That matters when the next step, such as closing a dialog or navigating, should only happen once the list is correct. ## Combining the options The options compose. An `update` function can make the change instant while `refetchQueries` confirms it from the server. The `onQueryUpdated` option runs once for each active query whose cached data the update changed. Returning `observableQuery.refetch()` from it re-checks that query, and returning `false` skips it. Adding `optimisticResponse` on top makes the change visible before the server has even answered. ## A decision rule - The mutation edits fields of issues already on screen: return those fields and do nothing else. - The mutation adds or removes an item from a list with simple, client-knowable order: write an `update` function. - The list's contents depend on server logic (filters, sorting, pagination, counts): use `refetchQueries`, possibly after an `update` for instant feedback. - A follow-up action needs the refreshed list: add `awaitRefetchQueries: true`.
- When is refetchQueries clearly the better choice than an update function?When list membership or order is computed on the server: filters, sorting, pagination windows, counts, or side effects on entities the mutation did not return. Reproducing that logic in `update` duplicates server rules and drifts. `refetchQueries` costs a request per query and a short delay, so it suits mutations that are rare compared with reads.
- What does the onQueryUpdated option add to a mutation?After the `update` function runs, Apollo calls `onQueryUpdated` once for each active query whose cached data changed. Return `observableQuery.refetch()` to confirm that query from the server, or `false` to skip it. If it returns a promise, the mutation's promise waits for it. It lets you refetch only the queries the update actually touched.
saying these in an interview costs you the question
- Returning the created issue from the mutation is enough to add it to every cached list.
- refetchQueries by operation name refetches a list even when no component is rendering it.
- awaitRefetchQueries makes the refetch start sooner.
- The update function only runs when refetchQueries is empty.
- refetchQueries: "all" is a sensible default for every mutation.