skip to content

In Apollo Client 4, the Author type has no id and is identified by a unique handle; how do you make InMemoryCache normalize it?

level: middleimportance: should knowfreq 42%

answer

  1. a policy per type
  2. name the identifying field
  3. JSON-shaped cache ID
  4. every selection needs the key

basics

~10 s

Declare the key in a type policy: typePolicies: { Author: { keyFields: ["handle"] } }. Authors are then stored under IDs like Author:{"handle":"ada"}, and every query that selects an Author must also select handle.

solid answer

~40 s

The default ID function only knows `id` and `_id`, so an Author identified by `handle` would be stored inline in every post. You fix that with a type policy: `new InMemoryCache({ typePolicies: { Author: { keyFields: ["handle"] } } })`. The cache then builds IDs like `Author:{"handle":"ada"}`, using the schema field name, so aliasing `handle` in a query does not change the ID. Every selection of Author must now include `handle`; if one does not, the cache write fails with a `Missing field 'handle' while extracting keyFields` error instead of storing a duplicate. `keyFields` also takes several fields, nested fields, an empty array for a singleton, `false` to switch normalization off, or a function. A global `dataIdFromObject` still exists as a fallback, but per-type `keyFields` is the recommended form.

code

ts · 15 lines
ts
import { InMemoryCache } from "@apollo/client";

export const cache = new InMemoryCache({
  typePolicies: {
    Author: {
      keyFields: ["handle"],
    },
    FeedSettings: {
      keyFields: [],
    },
  },
});

cache.identify({ __typename: "Author", handle: "ada" });
// 'Author:{"handle":"ada"}'

go deeper

for a junior

Recall that typePolicies with keyFields tell InMemoryCache which field identifies a type that has no id.

for a middle

Explain the resulting cache ID shape, why keyFields uses schema names rather than aliases, and what happens when a query does not select the key field.

for a senior

Choose between an array, false, an empty array and a function deliberately, prefer keyFields over a global dataIdFromObject, and use cache.identify rather than hand-built IDs in update code.

for a principal

Treat entity keys as part of the schema contract between client and API teams, so a type's identifying fields are agreed and always selectable before clients cache it.

## Why the default ID is not enough `InMemoryCache` identifies objects with a function called `defaultDataIdFromObject`, which reads `__typename` and then `id`, falling back to `_id`. When a type is identified by something else, such as an author's unique `handle`, that function returns nothing and the cache stores each author **inline** inside every post that mentions it. Renames and other updates then stop propagating across the feed. The symptom is easy to miss: nothing errors and the feed renders, and only a later write to one author reveals that each post holds its own copy. `cache.extract()` shows it directly, as posts whose `author` field contains a full object instead of a `__ref`. The fix is a **type policy**: per-type configuration passed to `InMemoryCache` through the `typePolicies` option, keyed by `__typename`. ## Declaring keyFields The `keyFields` property of a type policy says which fields identify an object of that type: ```ts import { InMemoryCache } from "@apollo/client"; const cache = new InMemoryCache({ typePolicies: { Author: { keyFields: ["handle"] }, }, }); ``` With this policy the author with handle `ada` is stored under the cache ID `Author:{"handle":"ada"}`: the type name, a colon, and a JSON object of the key fields. Things to know about how this works: - **Schema names, not aliases.** `keyFields` refers to the field names in the schema, so a query that aliases `handle` as `username` still produces the same ID. - **Stable order.** With several key fields, the ID always lists them in the order declared, so the same object always gets the same ID. - **The key must be selected.** If a result contains an Author without `handle`, the write fails with an invariant error, `Missing field 'handle' while extracting keyFields from …`. It does not silently store a second copy. - **`cache.identify` follows the policy.** `cache.identify({ __typename: "Author", handle: "ada" })` returns the same ID the cache uses, which saves you from building JSON-shaped IDs by hand for `cache.modify` or `cache.evict`. ## The other forms keyFields takes | Value | Example | Effect | |---|---|---| | array of fields | `["handle"]` | ID built from those fields | | several fields | `["platform", "handle"]` | composite key, both required | | nested field | `["title", "author", ["handle"]]` | uses a field of a child object | | empty array | `[]` | singleton: one object of the type, ID `FeedSettings:{}` | | `false` | `false` | normalization off; objects stay inline in their parent | | function | `(object, context) => …` | returns an ID string or a key array | The function form receives the object and a context with `typename`, `storeObject` and `readField`. The documentation recommends reading from `context.storeObject` or `context.readField`, which are already de-aliased, rather than from the raw object. ## dataIdFromObject: the global fallback `InMemoryCache` also accepts a `dataIdFromObject` option: one function that computes IDs for every type. It dates from Apollo Client 2 and remains for migration. A type policy's `keyFields` wins over it for its type; types without one fall back to it. The documentation lists its drawbacks: 1. It is sensitive to aliasing mistakes, because it sees the raw result object. 2. It does nothing to protect against undefined properties, so a missing field can produce an ID such as `Author:undefined`. 3. Using different key fields at different times causes inconsistent IDs. For these reasons, per-type `keyFields` arrays are the recommended way to key a type by a custom field. ## Sharing a key across related types If the feed's `Author` and `Page` types both implement an `Actor` interface and share the `handle` key, you can declare `possibleTypes: { Actor: ["Author", "Page"] }` on the cache and put `keyFields: ["handle"]` on `Actor` once. Type policies are inherited from supertypes named in `possibleTypes`, so both subtypes use the same key without repeating it. ## Checklist when introducing a custom key 1. Add the type policy with `keyFields` before shipping queries that rely on it. 2. Make sure every query, fragment and mutation result that returns the type selects the key field. 3. Use `cache.identify` in update code instead of formatting IDs yourself. 4. Check `cache.extract()` to confirm the type now appears as `Author:{"handle":…}` entities rather than embedded objects. 5. Remove any `dataIdFromObject` branch that used to handle the type, so exactly one place defines its key.

  • The post page runs `author(handle: $handle)`, and that author is already cached from the feed. How do you avoid a network request?
    Add a `read` function to the `Query.author` field policy that returns `toReference({ __typename: "Author", handle: args.handle })`. The cache then resolves the field to the existing `Author` entity. If every field the post page selects is already on that entity, the query is answered from the cache; if any field is missing, Apollo Client fetches from the network.
  • When is `keyFields: false` the right choice?
    When objects of a type have no stable identity of their own and only make sense inside their parent, such as a per-request summary. With `false` they are stored inline in the parent field. Because they are not entities, the cache will not merge them across queries, so a parent field that different queries select different subfields of may still need a merge function.

saying these in an interview costs you the question

  • Aliasing handle in one query gives that author a different cache ID.
  • keyFields false makes the type fall back to the default __typename plus id.
  • A global dataIdFromObject is the recommended way to key one type by a custom field.
  • If a query forgets handle, Apollo quietly stores a duplicate author.
  • keyFields must include id as well as the custom identifying field.