Cache identity, updating the cache after mutations and subscriptions, eviction, and tests
Every remote in a shell reads from one Apollo cache. The shell builds one client with createApolloClient, and createPreparedInMemoryCache (packages/util-apollo/src/cache.ts) creates its InMemoryCache. That cache sets only possibleTypes, from the schema's introspection, so fragments on interfaces and unions match. There are no typePolicies or keyFields today, so every type uses Apollo's default identity.
The examples on this page are illustrative. Message, messageAdded, and resolveChatroom aren't in the vendored schema, and the real Chatroom.messages is a paginated connection rather than a plain list, so check names and shapes against packages/data-gql/src/schema.graphql before copying.
| You want to… | Use |
|---|---|
| Change fields on an entity you already have | Nothing. Return the entity's id and the changed fields from the mutation or subscription, and the cache merges them |
| Add an entity to a list, or remove one from it | cache.modify on the list field, de-duplicating by reference |
| Rewrite a query's result in one go | cache.updateQuery |
| Write a whole entity you built yourself | cache.writeFragment |
| Remove a deleted entity everywhere | cache.evict({ id: cache.identify(entity) }), then cache.gc() |
| Make the server recompute something you can't (counts, filters, server-side sorting) | refetchQueries |
| Show the result before the server answers | optimisticResponse |
Find out why useFragment says complete: false | Why is useFragment incomplete? |
InMemoryCache stores each object that has a __typename and an id (or _id) once, under the key Typename:id, for example Chatroom:42. Every query, fragment, mutation, and subscription result that contains Chatroom:42 reads and writes that one entry. That is what makes updates show up everywhere without extra code.
id. The client adds __typename to every selection set, but it only knows id if you ask for it. The lint gate's require-selections rule makes every document select id on a type that has one.id isn't normalized. Its object is stored inside the field that returned it, as part of its parent. Two queries that return it can't share it, and useFragment can't find it on its own. The schema has many such types, mostly value objects and list wrappers (for example EmergencyContact or DispatchChatroomList). Read them through the parent's fragment.keyFields is the type policy that gives a type a different identity, such as keyFields: ["code"], or ["dispatchCenter", ["id"], "code"] for a nested key. Every document that returns the type must then select those fields; a write without them fails with Missing field '…' while extracting keyFields. keyFields: false stops a type from being normalized at all. We have no keyFields today. Add one only when a type without id has a stable key and two screens need to share it, and add it in createPreparedInMemoryCache with a test, never in a remote: the cache is shared, so a type policy applies to every remote in the shell.cache.identify(object) returns the cache key for an object ("Chatroom:42"), or undefined when it has no identity. Use it rather than building the string by hand: it follows keyFields if a type ever gets one.useFragment reads a fragment for one entity straight from the cache. It returns complete: true only when every field of the fragment is there; otherwise complete is false, data is partial, and missing says which fields are absent. There is no error. A component that checks complete (as it must) renders nothing. The usual causes, in order:
from has no id, because the type has none or the parent didn't select it. useFragment then has no cache key to read....Child_prop, so the child's fields were never fetched. With masking the parent can't read them either, so this shows up only as an incomplete child.from: { __typename: "Chatroom", id }) before any query has fetched that entity. A null from is also incomplete.fetchPolicy: "no-cache" never stores its result, so no child of it can read a fragment.messages(first: 10) and messages(first: 20) as two fields. A fragment that asks for different arguments from the ones the query fetched finds nothing.errorPolicy: "all", the data is partial and the field is null or missing.missing names the absent fields. Read it in the debugger before guessing. Don't cast your way past complete. If the data can arrive later, useSuspenseFragment suspends until it is complete instead.
The cache APIs are never masked: readFragment, writeFragment, updateQuery, updateFragment, and modify see and write whole objects, whatever the components' fragments hide.
When a mutation or subscription returns an entity with its id and the fields that changed, Apollo writes the result to the cache and merges those fields into the existing entry. Every query and fragment that reads them re-renders. Subscriptions do this too: a useSubscription result is written to the cache before your component sees it (unless fetchPolicy is "no-cache").
Select every field the mutation changes. A field you leave out keeps its old value in the cache.
Apollo can't know that a new message belongs in Chatroom.messages, or that a deleted one has left it. Change the list field with cache.modify, and de-duplicate by reference. The same entity can arrive twice: from a subscription and from your own mutation, or from a refetch.
id picks the entity, and fields maps each field name to a function that gets the stored value and returns the new one. A list of entities is stored as a list of references ({ __ref: "Message:7" }), so compare with readField("id", ref), not by object identity.DELETE, which removes the field, and INVALIDATE, which marks it stale and notifies its watchers without changing the value.modify runs your function for every stored variant of messages, so it must make sense for each of them.cache.modify doesn't add a field that isn't in the cache yet. If no query has fetched messages for that chatroom, there's nothing to update, and the next query fetches the fresh list.When the change is easier to describe as "this query's result, but different", updateQuery reads the cached result, passes it to your function, and writes back the new object you return. The data it passes is read-only, so build new objects rather than mutating it; return nothing to leave the cache as it is.
It changes only that query with those variables. To change a list wherever it appears, use modify on the owning entity's field. cache.updateFragment is the same tool for one entity's fragment.
writeFragment writes a fragment's fields for one entity, for example data that arrived outside GraphQL. Include __typename and id, or pass from (or id) to say which entity it is. It returns the entity's reference. A fragment used only for writeFragment is fine; it doesn't need a component.
refetchQueries sends the queries again and replaces their results. It costs a round trip, and the user waits for it, but it is right when the client can't compute the answer:
Pass the Documents, not their names as strings: a name with a typo refetches nothing, with only a development warning (Unknown query named …). Set awaitRefetchQueries: true when the mutation should stay loading until the refetch finishes. Prefer a cache update when the mutation already returns everything you need.
evict removes the entity. Lists that referenced it drop the dangling reference automatically when they're read, so you don't need to edit each list. A single (non-list) field that pointed at the entity becomes missing, and a cache-first query that reads it fetches again. cache.gc() then removes everything nothing can reach any more.
cache.evict({ id, fieldName: "messages" }) removes one field instead. Each active query that reads it fetches it again. That's a precise way to say "this list is stale" without refetching everything.
Apollo writes the optimistic result to a separate layer, renders it at once, and replaces it with the server's answer. If the mutation fails, it discards the layer and the UI goes back by itself. You still show the failure (see Error Handling). The optimistic object must have the shape of the mutation's whole result, including __typename and id. Masking doesn't apply to it. optimisticResponse can also be a function (variables, { IGNORE }) => …; return IGNORE to skip the optimistic update for that call. A mutation's update function runs for the optimistic result and again for the server's, so a list change made there must be idempotent: de-duplicate as above.
A type policy is configuration on InMemoryCache: keyFields for identity, and fields with read and merge functions per field. We have none. The rules if you need one:
createPreparedInMemoryCache with a unit test, not in a remote, because one cache serves every remote in the shell.messages again, the new list replaces the old one. That is right for a full list, and wrong for pages: page 2 would replace page 1. Paginated fields need a field policy with keyArgs (the arguments that identify different lists, as opposed to pages of one list) and a merge that combines pages. @apollo/client/utilities ships offsetLimitPagination, relayStylePagination, and concatPagination for the common shapes.id that two queries return differently is replaced, not merged, and Apollo warns Cache data may be lost when replacing the … field. The fix is to give the type an identity (keyFields), or merge: true on the type when it's safe to merge its fields.renderWithApollo (from @prepared911/util-testing) builds a real client with createApolloClient, so tests use the same cache as the app: createPreparedInMemoryCache, possibleTypes, data masking, and cache-first queries.
renderWithApollo call creates a new client and cache, so nothing leaks between tests. The returned client is that test's client: read its cache with client.cache.extract(), client.readFragment(…), or client.readQuery(…) to assert on a cache update.renderWithApollo(ui, { apollo: { cache } }). Data written with cache.writeQuery or cache.writeFragment needs __typename and id; the factories in @prepared911/data-gql/factories include both. A cache-first query that finds everything it needs makes no request.id with a mock override so both responses identify the same object.useFragment re-renders on a later tick after a cache write, so wait with findBy… or waitFor rather than asserting straight after the write.useFragmentOn this page