Prepared Docs

Prepared Docs

⌘K

    Loading navigation…

User

  1. Working With Data
  2. Graphql Cache

GraphQL Cache

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.

Quick reference

You want to…Use
Change fields on an entity you already haveNothing. 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 itcache.modify on the list field, de-duplicating by reference
Rewrite a query's result in one gocache.updateQuery
Write a whole entity you built yourselfcache.writeFragment
Remove a deleted entity everywherecache.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 answersoptimisticResponse
Find out why useFragment says complete: falseWhy is useFragment incomplete?

Identity

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.

  • Select 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.
  • A type without 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.

Why is useFragment incomplete?

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:

  1. The entity has no identity. The object passed as from has no id, because the type has none or the parent didn't select it. useFragment then has no cache key to read.
  2. The parent didn't spread the fragment. The query selected the entity but not ...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.
  3. The entity isn't in the cache. The query is still loading, or failed, or the component reads by id (from: { __typename: "Chatroom", id }) before any query has fetched that entity. A null from is also incomplete.
  4. The query didn't write to the cache. A query with fetchPolicy: "no-cache" never stores its result, so no child of it can read a fragment.
  5. A field's arguments differ. The cache stores 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.
  6. The entity was evicted, or a field on it was, after the query ran.
  7. The server returned an error for that field. With 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.

Updating the cache

The cache APIs are never masked: readFragment, writeFragment, updateQuery, updateFragment, and modify see and write whole objects, whatever the components' fragments hide.

Changing an entity: nothing to do

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.

Lists: cache.modify

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.
  • The modifier also receives DELETE, which removes the field, and INVALIDATE, which marks it stale and notifies its watchers without changing the value.
  • A field with arguments is stored once per set of arguments. 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.

A whole query result: cache.updateQuery

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.

An entity you built: cache.writeFragment

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.

When to refetch instead

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:

  • an aggregate, such as a count or a total;
  • a list the server filters, sorts, or paginates, where you can't tell where (or whether) the new item belongs;
  • a mutation with effects on many queries that you can't enumerate.

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.

Removing data

Eviction

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.

Optimistic responses

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.

Type policies and list merges

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:

  • It goes in createPreparedInMemoryCache with a unit test, not in a remote, because one cache serves every remote in the shell.
  • Lists replace by default. When a query returns 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.
  • An object without 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.

Cache behaviour in tests

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.

  • A fresh cache per render. Each 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.
  • Seeding. To start from data that's already cached, build a cache, write to it, and pass it in: 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.
  • Shared entities. The schema-driven mock handlers generate values, including ids. If a test needs two operations to meet on one entity (a mutation that updates what a query showed), pin the id with a mock override so both responses identify the same object.
  • Updates are asynchronous. useFragment re-renders on a later tick after a cache write, so wait with findBy… or waitFor rather than asserting straight after the write.

Related documentation

  • GraphQL Fragments - Component-owned fragments, masking, and useFragment
  • GraphQL Operations - Queries, mutations, and error handling
  • Pub/Sub - Subscriptions and live updates
  • Apollo's caching docs for API details. Apply this repo's layout and import rules to anything you take from them.

Previous

Working with data / GraphQL Development Workflow

Next

Working with data / Local Storage

On this page

Quick reference
Identity
Why is useFragment incomplete?
Updating the cache
Changing an entity: nothing to do
Lists: cache.modify
A whole query result: cache.updateQuery
An entity you built: cache.writeFragment
When to refetch instead
Removing data
Eviction
Optimistic responses
Type policies and list merges
Cache behaviour in tests
Related documentation