A GraphQL client's cache is what makes a screen update everywhere when a single object changes. After this lesson you will be able to explain how Apollo normalises results, update the cache precisely after mutations, and decide when urql or Relay is a better fit than Apollo.
InMemoryCache splits each result into flat records keyed by __typename and id, and stores the query as a set of references. A response containing { posts: [{ id: "1", title: "Hello", author: { id: "9", name: "Ada" } }] } becomes:
{
"ROOT_QUERY": { "posts({\"first\":10})": [{ "__ref": "Post:1" }] },
"Post:1": { "__typename": "Post", "id": "1", "title": "Hello", "author": { "__ref": "User:9" } },
"User:9": { "__typename": "User", "id": "9", "name": "Ada" }
}Because every object exists exactly once, a mutation returning { id: "9", name: "Ada L." } updates User:9, and every component that renders that user re-renders with the new name. No manual wiring is needed as long as operations select id (Apollo adds __typename automatically). Types without an id can be configured with typePolicies:
const cache = new InMemoryCache({
typePolicies: {
Book: { keyFields: ["isbn"] },
Query: { fields: { posts: relayStylePagination() } },
},
});relayStylePagination() from @apollo/client/utilities merges connection pages from fetchMore into one list.
Field updates are automatic, but adding or removing list entries is not, because the cache cannot know which lists a new object belongs to. The update callback edits the cache directly:
const [addPost] = useMutation(ADD_POST, {
update(cache, { data }) {
cache.updateQuery({ query: GET_POSTS, variables: { first: 10 } }, (existing) =>
existing ? { posts: [data.addPost, ...existing.posts] } : existing
);
},
});
const [deletePost] = useMutation(DELETE_POST, {
update(cache, { data }) {
cache.evict({ id: cache.identify({ __typename: "Post", id: data.deletePost.id }) });
cache.gc();
},
});cache.updateQuery rewrites one stored query result, cache.modify edits fields of a record, cache.evict removes a record and cache.gc collects unreferenced ones. optimisticResponse applies the same update immediately with a predicted result, then replaces it when the server answers.
urql is a lighter client built around a pipeline of exchanges. By default it uses a document cache: results are cached per query, and any mutation that returns a __typename present in a cached query invalidates that query. That is often enough:
import { Client, Provider, cacheExchange, fetchExchange, useQuery } from "urql";
const client = new Client({ url: "http://localhost:4000/graphql", exchanges: [cacheExchange, fetchExchange] });
const [{ data, fetching, error }] = useQuery({ query: GET_POSTS, variables: { first: 10 } });When you need Apollo-style normalization, swap cacheExchange for Graphcache from @urql/exchange-graphcache, which also supports offline persistence and optimistic updates.
Relay, Meta's client, is built for very large applications. Its compiler processes every operation at build time: each component declares a fragment for exactly the data it renders, and the parent query is assembled automatically. It requires schema conventions (globally unique id fields, a Node interface, Relay-style connections). The payoff is guaranteed data masking and minimal runtime work; the cost is a steeper setup.
| Client | Cache | Setup | Best for | |---|---|---|---| | Apollo Client | Normalized by default | Moderate | Most React apps, largest ecosystem | | urql | Document cache, Graphcache optional | Minimal | Smaller bundles, multiple frameworks | | Relay | Normalized, compiler-enforced | Heavy | Large teams with strict conventions |
Why does a mutation returning `{ id: "9", name: "Ada L." }` update every component that shows user 9?
InMemoryCache normalises results into records keyed by __typename and id; select id in every operation.update with cache.updateQuery or cache.evict for list changes.typePolicies handle custom keys and pagination merging such as relayStylePagination().Next lesson: TypeScript and GraphQL Code Generator — generate resolver and operation types from your schema so the compiler catches mismatches.