Ricard Fredin

Stop Mixing cache.modify with refetchQueries

Apollo gives you two ways to keep the cache fresh after a mutation. Using both at the same time is an anti-pattern — and the cleanup is usually a net deletion.

• 4 min read

If you’ve worked in an Apollo codebase for more than a year, you’ve probably seen mutations that look like this:

const [archiveCandidate] = useMutation(ARCHIVE_CANDIDATE, {
  update(cache, { data }) {
    cache.modify({
      id: cache.identify({ __typename: "Candidate", id }),
      fields: { archived: () => true },
    });
  },
  refetchQueries: ["CandidateList", "CandidateCount"],
});

Both update and refetchQueries. Belt and braces. It “works” — the UI updates and the data is correct.

It’s also wrong, and over the last quarter we ripped this pattern out of about eight mutations in our codebase. The net was around 130 lines deleted and a measurable drop in post-mutation network requests.

Why it’s wrong

Apollo gives you two distinct strategies for keeping the cache consistent after a mutation:

  1. Optimistic / surgical cache updatesupdate, cache.modify, cache.writeFragment. You tell Apollo exactly what changed. Cheap, instant, no network round trip.
  2. RefetchrefetchQueries. You tell Apollo “go ask the server again.” Slower, but bulletproof for cases where you can’t compute the new state on the client.

Pick one per mutation. They solve the same problem with different trade-offs.

When you do both:

The decision rule

We wrote this down as an ADR to stop relitigating it in every code review:

If you can compute the post-mutation state on the client, use update / cache.modify. If you can’t (server-computed fields, aggregations, ordering you don’t control), use refetchQueries. Never both.

The forcing function is the “can you compute it” question. Most of the time you can, and the surgical update is a few lines. The cases where you genuinely can’t — counts, server-side rankings, cross-entity invariants — are the cases where refetchQueries earns its keep.

What we found while cleaning up

A few patterns kept showing up:

Redundant org-ID threading. Mutations were passing organizationId purely so refetchQueries could re-issue scoped queries. Once we dropped the refetch, the prop disappeared from three or four layers of components.

Subscription-backed queries getting refetched anyway. If you have a subscription keeping a query live, refetching on mutation is doubly redundant — the subscription will deliver the update. We had this in several places.

“Just in case” refetches. The most common one. Someone added refetchQueries because they weren’t sure the cache update was complete, and it stuck around forever. In most cases the cache update was fine.

The smell

If you’re tempted to add refetchQueries to a mutation that already has update, stop and ask: why don’t I trust my own cache update? Usually one of:

None of those are reasons to keep both. They’re reasons to pick the right one and delete the other.

Takeaway

Apollo cache strategy is one of those things every team rediscovers the hard way. The fix isn’t more cleverness — it’s picking a rule, writing it down, and letting code review enforce it.

In our case, the rule was a net deletion. That’s usually the sign you’re on the right track.