Keep Apollo Client Cache in Sync After a Mutation Without Refetching
Use a cache update function with Apollo Client InMemoryCache to merge mutation results locally and avoid unnecessary refetches. Includes verification steps and limits.
16 Jan 2026, 03:07 UTC

The problem: a mutation that the UI ignores
You call a GraphQL mutation to rename a project. The server returns the updated object, but the project list still shows the old name. You can force a refetch, but that costs another network round-trip and often causes a visible flicker while the list reloads.
The useful takeaway: in Apollo Client v3.x, you can merge the mutation result directly into the normalized InMemoryCache using an update function. The UI updates from cache, and you can verify there is no extra refetch.
Why the list stays stale
Apollo Client’s InMemoryCache normalizes cached objects using __typename plus an identifier such as id or _id. A query for a project list stores records like Project:123. When a mutation returns updateProject with the same __typename and id, Apollo Client can match it to the existing normalized record and update it in place.
If the mutation response omits __typename or id, Apollo Client cannot match the existing record. It may store a separate fragment instead, leaving the list unchanged while the cache holds a duplicate.
Write an update function for the mutation
Use the update option on useMutation. It runs client-side after the mutation resolves and does not execute server resolvers or confirm server-side state.
Where to run: in a React component file in a project using @apollo/client v3.x. No special permissions are required.
import { useMutation, gql } from '@apollo/client';
const UPDATE_PROJECT = gql`
mutation UpdateProject($id: ID!, $name: String!) {
updateProject(id: $id, name: $name) {
id
name
}
}
`;
export function ProjectRow({ project }) {
const [updateProject] = useMutation(UPDATE_PROJECT, {
update(cache, { data }) {
const updated = data?.updateProject;
if (!updated) return;
cache.modify({
id: cache.identify({ __typename: 'Project', id: updated.id }),
fields: {
name: () => updated.name,
},
});
},
});
return (
updateProject({ variables: { id: project.id, name: 'New name' } })}
>
Rename
);
}
cache.identify builds the normalized cache key from __typename and id. cache.modify then replaces only the name field on that record. Any query reading Project:123 sees the new value on the next render.
Verify the behavior
- Open Apollo Client DevTools in the browser. Confirm one normalized record exists for the project and that its
namefield changed after the mutation. - Check the network panel before and after adding the update function. There should be no extra refetch for the same entity.
- Run
npm ls @apollo/clientin your project directory to confirm v3.x is installed. v2 used different cache APIs and is not interchangeable. - For a unit test, render the component with
MockedProvider—a testing component that mocks Apollo Client—supply a mutation result containingidand__typename, then assertcache.readFragmentreturns the updated values.
Expected checks: the cache shows a single normalized record, the UI reflects the new name immediately, and the network tab shows only the mutation request.
Trade-offs and limits
- The update function cannot validate server state. If the mutation succeeds locally but the server later rejects it, the cache and UI can diverge.
- Nested mutation results do not automatically inherit query field policies. Address the target path explicitly in the update function.
- Because this changes client-side cache state, a practical rollback is to refetch the affected query so the cache reconciles with the server.
Closing
Start with one mutation where the response includes id and __typename. Add an update function, verify with DevTools and the network panel, then expand to other writes. If the mutation result lacks key fields, fix the server response or the type policy before relying on cache updates.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.