Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation for custom FieldPolicy read and merge functions. #5677

Merged
merged 10 commits into from
Jan 29, 2020
5 changes: 4 additions & 1 deletion docs/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ module.exports = {
'data/fragments',
'data/error-handling',
],
Caching: ['caching/cache-configuration', 'caching/cache-interaction'],
Caching: [
'caching/cache-configuration',
'caching/cache-field-behavior',
'caching/cache-interaction'],
'Development & Testing': [
'development-testing/static-typing',
'development-testing/testing',
Expand Down
141 changes: 15 additions & 126 deletions docs/source/caching/cache-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Configuring the cache

Apollo Client stores the results of its GraphQL queries in a normalized, in-memory cache. This enables your client to respond to future queries for the same data without sending unnecessary network requests.

>This article describes cache setup and configuration. To learn how to interact with cached data, see [Interacting with cached data](cache-interaction/).
>This article describes cache setup and configuration. To learn how to interact with cached data, see [Interacting with cached data](./cache-interaction).

## Installation

Expand Down Expand Up @@ -52,7 +52,7 @@ The `InMemoryCache` **normalizes** query response objects before it saves them t

1. The cache [generates a unique ID](#generating-unique-identifiers) for every identifiable object included in the response.
2. The cache stores the objects by ID in a flat lookup table.
3. Whenever an object is stored with the same ID as an _existing_ object, the fields of those objects are _merged_. The new object overwrites the values of any fields that appear in both.
3. Whenever an incoming object is stored with the same ID as an _existing_ object, the fields of those objects are _merged_. If the incoming object and the existing object share any fields, the incoming object _overwrites_ the cached values for those fields. Fields that appear _only_ in the existing object _or_ the incoming object are preserved.

Normalization constructs a partial copy of your data graph on your client, in a format that's optimized for reading and updating the graph as your application changes state.

Expand All @@ -62,13 +62,13 @@ Normalization constructs a partial copy of your data graph on your client, in a

#### Default identifier generation

By default, the `InMemoryCache` generates a unique identifier for any object that includes a `__typename` field by combining the object's `__typename` with its `id` or `_id` field (whichever is defined). These two values are separated by a colon (`:`).
By default, the `InMemoryCache` generates a unique identifier for any object that includes a `__typename` field. To do so, it combines the object's `__typename` with its `id` or `_id` field (whichever is defined). These two values are separated by a colon (`:`).

For example, an object with a `__typename` of `Task` and an `id` of `14` is assigned a default identifier of `Task:14`.

#### Customizing identifier generation by type

If one of your types defines its primary key with a field besides `id` or `_id`, you can customize how the `InMemoryCache` generates its unique identifier by defining a `TypePolicy` for the type. You specify all of your cache's `typePolicies` in [the `options` object you provide to the `InMemoryCache` constructor](#configuring-the-cache).
If one of your types defines its primary key with a field _besides_ `id` or `_id`, you can customize how the `InMemoryCache` generates unique identifiers for that type. To do so, you define `TypePolicy` for the type. You specify all of your cache's `typePolicies` in [the `options` object you provide to the `InMemoryCache` constructor](#configuring-the-cache).

Include a `keyFields` field in relevant `TypePolicy` objects, like so:

Expand Down Expand Up @@ -97,52 +97,21 @@ const cache = new InMemoryCache({

This example shows three `typePolicies`: one for a `Product` type, one for a `Person` type, and one for a `Book` type. Each `TypePolicy`'s `keyFields` array defines which fields on the type _together_ represent the type's primary key.

Note that the `Book` type uses a _subfield_ as part of its primary key. The `["name"]` item indicates that the `name` field of the _previous_ field in the array (`author`) is part of the primary key. The `Book`'s `author` field must be an object that includes a `name` field for this to be valid.
The `Book` type above uses a _subfield_ as part of its primary key. The `["name"]` item indicates that the `name` field of the _previous_ field in the array (`author`) is part of the primary key. The `Book`'s `author` field must be an object that includes a `name` field for this to be valid.

In the example above, the unique identifier string for a `Book` object has the following format:
In the example above, the resulting identifier string for a `Book` object has the following structure:

```
Book:{"title":"Fahrenheit 451","author":{"name":"Ray Bradbury"}}
```

The object's primary key fields are always listed in the same order to ensure uniqueness.
An object's primary key fields are always listed in the same order to ensure uniqueness.

Note that these `keyFields` strings always refer to the actual field names as defined in your schema, meaning the ID computation is not sensitive to [field aliases](https://www.apollographql.com/docs/resources/graphql-glossary/#alias). This note is important if you ever attempt to use a function to implement `keyFields`:

```ts
const cache = new InMemoryCache({
typePolicies: {
Person: {
keyFields(responseObject, { typename, selectionSet, fragmentMap }) {
let id: string | null = null;
selectionSet.selections.some(selection => {
if (selection.kind === 'Field') {
// If you fail to take aliasing into account, your custom
// normalization is likely to break whenever a query contains
// an alias for key field.
const actualFieldName = selection.name.value;
const responseFieldName = (selection.alias || selection.name).value;
if (actualFieldName === 'socialSecurityNumber') {
id = `${typename}:${responseObject[responseFieldName]}`;
return true;
}
} else {
// Handle fragments using the fragmentMap...
}
return false;
});
return id;
},
},
},
});
```

If this edge case seems obscure, you should probably steer clear of implementing your own `keyFields` functions, and instead stick to passing an array of strings for `keyFields`, so that you never have to worry about subtle bugs like these. As a general rule, the `typePolicies` API allows you to configure normalization behavior in one place, when you first create your cache, and does not require you to write your queries differently by aliasing fields or using directives.
Note that these `keyFields` strings always refer to the actual field names as defined in your schema, meaning the ID computation is not sensitive to [field aliases](https://www.apollographql.com/docs/resources/graphql-glossary/#alias).

#### Customizing identifier generation globally

If you need to define a single fallback `keyFields` function that isn't specific to any particular `__typename`, the old `dataIdFromObject` function from Apollo Client 2.x is still supported:
If you need to define a single fallback `keyFields` function that isn't specific to any particular `__typename`, you can use the `dataIdFromObject` function that was introduced in Apollo Client 2.x:

```ts
import { defaultDataIdFromObject } from '@apollo/client';
Expand All @@ -158,17 +127,17 @@ const cache = new InMemoryCache({
});
```

Notice how this function ends up needing to select different keys based on specific `object.__typename` strings anyway, so you might as well have used `keyFields` arrays for the `Product` and `Person` types via `typePolicies`. Also, this code is sensitive to aliasing mistakes, it does nothing to protect against undefined `object` properties, and accidentally using different key fields at different times could cause inconsistencies in the cache.
> The `dataIdFromObject` API is included in Apollo Client 3.0 to ease the transition from Apollo Client 2.x. The API might be removed in a future version of `@apollo/client`.

The `dataIdFromObject` API is meant to ease the transition from Apollo Client 2.x to 3.0, and may be removed in future versions of `@apollo/client`.
Notice that the above function still uses different logic to generate keys based on an object's `__typename`. In the above case, you might as well define `keyFields` arrays for the `Product` and `Person` types via `typePolicies`. Also, this code is sensitive to aliasing mistakes, it does nothing to protect against undefined `object` properties, and accidentally using different key fields at different times can cause inconsistencies in the cache.

### Disabling normalization

You can instruct the `InMemoryCache` _not_ to normalize objects of a certain type. This might make sense for metrics and other transient data that are identified by a timestamp and never receive updates.
You can instruct the `InMemoryCache` _not_ to normalize objects of a certain type. This can be useful for metrics and other transient data that's identified by a timestamp and never receives updates.

To disable normalization for a type, define a `TypePolicy` for the type (as shown in [Customizing identifier generation by type](#customizing-identifier-generation-by-type)), but set the policy's `keyFields` field to `false`.
To disable normalization for a type, define a `TypePolicy` for the type (as shown in [Customizing identifier generation by type](#customizing-identifier-generation-by-type)) and set the policy's `keyFields` field to `false`.

Objects that are not normalized are instead embedded within their _parent_ object in the cache. You cannot access these objects directly and must instead access them via their parent.
Objects that are not normalized are instead embedded within their _parent_ object in the cache. You can't access these objects directly, but you can access them via their parent.

## The `TypePolicy` type

Expand Down Expand Up @@ -257,84 +226,4 @@ Compared to the `__typename`s of entity objects like `Book`s or `Person`s, which

### The `fields` property

The final property within `TypePolicy` is the `fields` property, which is a map from string field names to `FieldPolicy` objects. The next section covers field policies in depth.

## Field policies

In addition to configuring the identification and normalization of `__typename`-having entity objects, a `TypePolicy` can provide policies for any of the fields supported by that type.

Here are the `FieldPolicy` type and its related types:

```ts
export type FieldPolicy<TValue> = {
keyArgs?: KeySpecifier | KeyArgsFunction;
read?: FieldReadFunction<TValue>;
merge?: FieldMergeFunction<TValue>;
};

type KeyArgsFunction = (
field: FieldNode,
context: {
typename: string;
variables: Record<string, any>;
},
) => string | null | void;

type FieldReadFunction<TExisting, TResult = TExisting> = (
existing: Readonly<TExisting> | undefined,
options: FieldFunctionOptions,
) => TResult;

type FieldMergeFunction<TExisting> = (
existing: Readonly<TExisting> | undefined,
incoming: Readonly<StoreValue>,
options: FieldFunctionOptions,
) => TExisting;

interface FieldFunctionOptions {
args: Record<string, any>;
parentObject: Readonly<StoreObject>;
field: FieldNode;
variables?: Record<string, any>;
}
```

In the sections below, we will break down these types with explanations and examples.

### Key arguments

Similar to the `keyFields` property of `TypePolicy` objects, the `keyArgs` property of `FieldPolicy` objects tells the cache which arguments passed to the field are "important" in the sense that their values (together with the enclosing entity object) determine the field's value.

By default, the cache assumes all field arguments might be important, so it stores a separate field value for each unique combination of argument values it has received for that field. This is a safe policy because it ensures field values do not collide with each other if there was any difference in their arguments. However, this policy can also lead to unnecessary copies of field values, as well as missed opportunities for fields to share the same logical value even if their arguments were slightly different.

For example, imagine you have a field that returns a secret value according to a given key, but also requires an access token to authenticate the request:

```ts
query GetSecret {
secret(key: $secretKey, token: $secretAccessToken) {
message
}
}
```

As long as you have a valid access token, the value of this field depends only on the `key`. In other words, you won't get a different secret message back just because you used a different (valid) token.

In cases like this, it would be wasteful and potentially inconsistent to let the access token factor into the storage of the field value, so you should let the cache know that only `key` is "important" by using the `keyArgs` option of the `FieldPolicy` for the `secret` field:

```ts
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
secret: {
keyArgs: ["key"],
},
},
},
},
});
```

That said, you might be able to assume the `token` is always the same, or you might not be worried about duplicating field values in the cache, so neglecting to specify `keyArgs: ["key"]` probably will not cause any major problems. Use `keyArgs` when it helps.

On the other hand, perhaps you've requested the secret from the server using the access `token`, but you want various components on your page to be able to access the secret using only they `key`, without having to know the `token`. Storing the value in the cache using only the `key` makes this retrieval possible.
The final property within `TypePolicy` is the `fields` property, which is a map from string field names to `FieldPolicy` objects. For more information on this field, see [Customizing the behavior of cached fields](./cache-field-behavior).
Loading