In Apollo Client 4, the Author type has no id and is identified by a unique handle; how do you make InMemoryCache normalize it?
answer
- a policy per type
- name the identifying field
- JSON-shaped cache ID
- every selection needs the key
basics
~10 sDeclare 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 sThe 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 linesimport { 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
Recall that typePolicies with keyFields tell InMemoryCache which field identifies a type that has no id.
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.
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.
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.