Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .size-limits.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43857,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38699,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33415,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27498
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43872,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38703,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33436,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27453
}
55 changes: 55 additions & 0 deletions docs/source/local-state/managing-state-with-field-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,61 @@

> If you query a local-only field that _doesn't_ define a `read` function, Apollo Client performs a default cache lookup for the field. See [Storing local state in the cache](#storing-local-state-in-the-cache) for details.

<Caution>

The initial `existing` value provided to the `read` function is `null` instead of `undefined` when the `@client` field is a child of another server field. This means using default parameters to set a default value doesn't work as expected and instead returns `null` as the value for the field. Note that this behavior doesn't affect root `@client` fields which receive `undefined` as the initial `existing` value.<br/><br/>

Check warning on line 89 in docs/source/local-state/managing-state-with-field-policies.mdx

View check run for this annotation

Apollo Librarian / AI Style Review

docs/source/local-state/managing-state-with-field-policies.mdx#L89

The original phrasing is grammatically awkward because the subject of the verb 'returns' is unclear. This change rephrases the sentence for better clarity and grammatical correctness. ```suggestion The initial <code>existing</code> value provided to the <code>read</code> function is <code>null</code> instead of <code>undefined</code> when the <code>@client</code> field is a child of another server field. This means using default parameters to set a default value doesn't work as expected, and the field's value is <code>null</code> instead. Note that this behavior doesn't affect root <code>@client</code> fields which receive <code>undefined</code> as the initial <code>existing</code> value.<br/><br/> ```

<ExpansionPanel title="Understanding this behavior">

When you're using a `fetchPolicy` that reads from the cache, Apollo Client first tries to fulfill the query from the cache before executing the query on the network. In the context of `@client` fields, `LocalState` is treated as part of the network layer when resolving field values.<br/><br/>

`LocalState` fulfills values for `@client` fields by first running local resolvers. If a local resolver isn't provided for a particular `@client` field, `LocalState` then tries to resolve the value from the cache. If the cache doesn't contain a value for the field, the field value is set to `null` to ensure future cache reads don't result in a cache miss.<br/><br/>

When using `read` functions to resolve root `@client` fields, those `read` functions run as a result of executing the initial cache read. If a value hasn't yet been written to the cache, the `existing` value is set to `undefined`, allowing default parameters to provide a default value for the field.<br/><br/>

When using `read` functions to resolve child `@client` fields of other server fields, `read` functions might not be run as part of the initial cache read when the parent field doesn't return a cached value. The query is then executed on the network, which includes running the query through `LocalState` to fulfill data for `@client` fields. In this scenario, `LocalState` resolves the value of the `@client` field to `null` before the `read` function is run. Unless your `merge` function alters the `null` value before it's written to the cache, the `existing` value provided to the `read` function is `null`.

Check warning on line 99 in docs/source/local-state/managing-state-with-field-policies.mdx

View check run for this annotation

Apollo Librarian / AI Style Review

docs/source/local-state/managing-state-with-field-policies.mdx#L99

This paragraph explains a complex process. This change improves readability by making the sentences more direct and concise. ```suggestion The <code>read</code> functions for child <code>@client</code> fields might not run during the initial cache read if their parent field has no cached value. Instead, the query is executed on the network, which runs the query through <code>LocalState</code> to provide data for <code>@client</code> fields. In this scenario, <code>LocalState</code> resolves the <code>@client</code> field's value to <code>null</code> before its <code>read</code> function runs. Unless your <code>merge</code> function alters this <code>null</code> value before it's written to the cache, the <code>existing</code> value provided to the <code>read</code> function is <code>null</code>. ```

</ExpansionPanel>

To provide default values for `read` functions on `@client` fields, use the nullish coalescing operator:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we can help with by providing a dev-only warning?

It's a bit tricky, but this should catch most cases:

const no_args_or_spread_function = /^(function\s+)?([\p{ID_Start}$_][\p{ID_Continue}$]*\s*)?\(\s*([)]|\.\.\.[\p{ID_Start}$_][\p{ID_Continue}$]*)/u
if (readPolicyFn.length === 0 && !readPolicyFn.toString().match(no_args_or_spread_function)) {
  // warn about using default arguments
}

I think this should eliminate all weird one-offs, but an additional check for a general function shape of the string might make it even more solid so it doesn't warn on engine builtins (but why would one pass those in there?)


```ts
new InMemoryCache({
typePolicies: {
Product: {
fields: {
isInCart: {
// ❌ Don't use default parameter values, otherwise the
// value will be null
read(value = false) {
return value;
},

// ✅ Use nullish coalescing operator to set a default value
read(value) {
return value ?? false;
},
},
},
},

Query: {
fields: {
rootClientField: {
// ✅ Default parameters on root client field
// read functions work as expected
read(value = "rootFieldDefault") {
return value;
},
},
},
},
},
});
```

</Caution>

### Reads are synchronous by design

Many UI frameworks like React (when not using `Suspense`) have synchronous rendering pipelines, therefore it's important for UI components to have immediate access to any existing data. This is why all `read` functions are synchronous, as are the cache's `readQuery` and `readFragment` methods. It is possible, however, to leverage reactive variables and `options.storage` to compose a `read` function that behaves in a manner that resembles an asynchronous action:
Expand Down