Skip to content

Commit 8e453f1

Browse files
committed
documentation updates for LocalState
1 parent 18688f6 commit 8e453f1

File tree

4 files changed

+224
-24
lines changed

4 files changed

+224
-24
lines changed

.claude/documentation.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,22 @@ Spelling and grammar should follow American English conventions.
2626
Content should be framed towards the reader in an active voice (e.g. "You can use the `useQuery` hook to fetch data" instead of "The `useQuery` hook can be used to fetch data"). Passive voice can be used to describe the behavior of the library (e.g. "The `useQuery` hook returns an object containing the query result") as a result of userland code usage.
2727
The use of "we" should be avoided, unless it is used to refer to the Apollo Client team as a whole (e.g. "We recommend using fragment colocation and data masking.").
2828

29-
### Change documentation
30-
If documentation is updated, it should always describe the new status quo and not contain statements like "previously" or "previous behavior".
29+
### Change documentation - CRITICAL RULE
30+
**NEVER reference changes, previous versions, or historical behavior in documentation.** Documentation must ONLY describe the current state of the library.
31+
32+
FORBIDDEN phrases and patterns:
33+
- "previously" or "previous behavior"
34+
- "has been restructured" or "has been changed"
35+
- "now takes" or "now provides"
36+
- "in earlier versions"
37+
- "used to be" or "was changed"
38+
- Any reference to what something was like before
39+
40+
CORRECT approach:
41+
- Describe how things work today
42+
- State what the current behavior is
43+
- Explain the current API without comparing to past versions
44+
3145
If new features are added to the documentation, they can include a `<MinVersion version="3.10.0">` tag wrapping the headline of the new feature, to indicate that this feature is only available in Apollo Client 3.10.0 and later.
3246
For features that are introduced in a major version, the `<MinVersion>` tag should not be used, as for each major version, a separate copy of the documentation is maintained.
3347
Currently, this documentation copy is the copy for Apollo Client with the major version 4, so all references to previous versions can be removed.
@@ -40,3 +54,11 @@ Never use phrases like "since version 4.0, this or that change applied". Only do
4054

4155
### Updating DocBlocks
4256
If outdated or incorrect documentation is found in `docs/public/client.api.json`, find the closest parent `fileUrlPath` in the JSON structure and update the original DocBlock in that file. Then run `npm run docmodel` to regenerate the JSON file and read the `docs/public/client.api.json` again.
57+
58+
### DocBlock Guidelines
59+
- Use `@template` to document TypeScript generics, not `@param`
60+
- Example: `@template TData - The type of data returned by the query`
61+
- Use `@param` only for actual function parameters
62+
- Use `@returns` to document return values
63+
- Use `@defaultValue` to document default values for properties in interfaces describing options
64+
- Use `@example` for code examples

docs/source/local-state/local-resolvers.mdx

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,20 @@ fieldName: (obj, args, context, info) => result;
8787

8888
1. `obj`: The object containing the result returned from the resolver on the parent field or the `ROOT_QUERY` object in the case of a top-level query or mutation.
8989
2. `args`: An object containing all of the arguments passed into the field. For example, if you called a mutation with `updateNetworkStatus(isConnected: true)`, the `args` object would be `{ isConnected: true }`.
90-
3. `context`: An object of contextual information shared between your React components and your Apollo Client network stack. In addition to any custom context properties that may be present, local resolvers always receive the following:
91-
- `context.client`: The Apollo Client instance.
92-
- `context.cache`: The Apollo Cache instance, which can be used to manipulate the cache with `context.cache.readQuery`, `.writeQuery`, `.readFragment`, `.writeFragment`, `.modify`, and `.evict`. You can learn more about these methods in [Managing the cache](#managing-the-cache).
93-
- `context.getCacheKey`: Get a key from the cache using a `__typename` and `id`.
90+
3. `context`: The context argument provides additional information without the possibility of name clashes. It takes the following shape:
91+
```ts
92+
{
93+
// The request context. By default this is of type `DefaultContext`,
94+
// but can be changed if a `context` function is provided to LocalState
95+
requestContext: TContextValue,
96+
// The client instance making the request
97+
client: ApolloClient,
98+
// Whether the resolver is run as a result of gathering exported variables
99+
// or resolving the value as part of the result
100+
phase: "exports" | "resolve"
101+
}
102+
```
103+
To access the cache, use `context.client.cache`.
94104
4. `info`: Information about the execution state of the query. You will probably never have to use this one.
95105

96106
Let's take a look at an example of a resolver where we toggle a todo's completed status:
@@ -103,9 +113,9 @@ const client = new ApolloClient({
103113
localState: new LocalState({
104114
resolvers: {
105115
Mutation: {
106-
toggleTodo: (_root, variables, { cache }) => {
107-
cache.modify({
108-
id: cache.identify({
116+
toggleTodo: (_root, variables, { client }) => {
117+
client.cache.modify({
118+
id: client.cache.identify({
109119
__typename: 'TodoItem',
110120
id: variables.id,
111121
}),
@@ -121,10 +131,62 @@ const client = new ApolloClient({
121131
});
122132
```
123133

124-
In previous versions of Apollo Client, toggling the `completed` status of the `TodoItem` required reading a fragment from the cache, modifying the result by negating the `completed` boolean, and then writing the fragment back into the cache. Apollo Client 3.0 introduced the `cache.modify` method as an easier and faster way to update specific fields within a given entity object. To determine the ID of the entity, we pass the `__typename` and primary key fields of the object to `cache.identify` method.
134+
To determine the ID of the entity, we pass the `__typename` and primary key fields of the object to `cache.identify` method. The `cache.modify` is used to update specific fields within a given entity object.
125135

126136
Once we toggle the `completed` field, since we don't plan on using the mutation's return result in our UI, we return `null` since all GraphQL types are nullable by default.
127137

138+
### Type safety with LocalState
139+
140+
The `LocalState` class accepts a `Resolvers` generic that provides autocompletion and type checking against your resolver types to ensure your resolvers are type-safe:
141+
142+
```ts
143+
import { LocalState } from "@apollo/client/local-state";
144+
import { Resolvers } from "./path/to/local-resolvers-types.ts";
145+
146+
// LocalState accepts a `Resolvers` generic.
147+
const localState = new LocalState<Resolvers>({
148+
resolvers: {
149+
// Your resolvers will be type-checked against the Resolvers type
150+
}
151+
});
152+
```
153+
154+
You may also pass a `ContextValue` generic used to ensure the `context` function returns the correct type. This type is inferred from your resolvers if not provided:
155+
156+
```ts
157+
new LocalState<Resolvers, ContextValue>({
158+
// The return value of this function will be type-checked
159+
context: (options) => ({
160+
// ...
161+
}),
162+
resolvers: {
163+
// ...
164+
}
165+
});
166+
```
167+
168+
### Error handling
169+
170+
Throwing errors in a resolver will set the field value as `null` and add an error to the response's `errors` array. This follows GraphQL's standard error handling behavior:
171+
172+
```ts
173+
const localState = new LocalState({
174+
resolvers: {
175+
Query: {
176+
riskyField: () => {
177+
throw new Error("Something went wrong!");
178+
// This will result in: { data: { riskyField: null }, errors: [...] }
179+
}
180+
}
181+
}
182+
});
183+
```
184+
185+
### Data handling
186+
187+
- **Remote result dealiasing**: Remote results are dealiased before they are passed as the parent object to a resolver so that you can access fields by their field name.
188+
- **Null data handling**: If the server returns `data: null` or does not provide a result, your local resolvers will not be called.
189+
128190
Let's learn how to trigger our `toggleTodo` mutation from our component:
129191

130192
```jsx
@@ -289,8 +351,8 @@ const GET_LAUNCH_DETAILS = gql`
289351

290352
This query includes a mixture of both remote and local fields. `isInCart` is the only field marked with an `@client` directive, so it's the field we'll focus on. When Apollo Client executes this query and tries to find a result for the `isInCart` field, it runs through the following steps:
291353

292-
1. Has a resolver function been set (either through the `ApolloClient` constructor `resolvers` parameter or Apollo Client's `setResolvers` / `addResolvers` methods) that is associated with the field name `isInCart`? If yes, run and return the result from the resolver function.
293-
2. If a matching resolver function can't be found, check the Apollo Client cache to see if a `isInCart` value can be found directly. If so, return that value.
354+
1. Has a resolver function been set for the field name `isInCart`? If yes, run and return the result from the resolver function.
355+
2. If a matching resolver function can't be found, check the Apollo Client cache to see if a `isInCart` value can be found directly or read with a `read` field `TypePolicy`. If so, return that value.
294356

295357
Let's look at both of these steps more closely.
296358

@@ -350,22 +412,22 @@ const GET_LAUNCH_DETAILS = gql`
350412
// ... run the query using client.query, a <Query /> component, etc.
351413
```
352414

353-
Here when the `GET_LAUNCH_DETAILS` query is executed, Apollo Client looks for a local resolver associated with the `isInCart` field. Since we've defined a local resolver for the `isInCart` field in the `ApolloClient` constructor, it finds a resolver it can use. This resolver function is run, then the result is calculated and merged in with the rest of the query result (if a local resolver can't be found, Apollo Client will check the cache for a matching field - see [Local data query flow](#local-data-query-flow) for more details).
415+
Here when the `GET_LAUNCH_DETAILS` query is executed, Apollo Client looks for a local resolver associated with the `isInCart` field. Since we've defined a local resolver for the `isInCart` field in the `LocalState` instance, it finds a resolver it can use. This resolver function is run, then the result is calculated and merged in with the rest of the query result (if a local resolver can't be found, Apollo Client will check the cache for a matching field - see [Local data query flow](#local-data-query-flow) for more details).
354416

355-
Setting resolvers through `ApolloClient`'s constructor `resolvers` parameter, or through its `setResolvers` / `addResolvers` methods, adds resolvers to Apollo Client's internal resolver map (refer to the [Local resolvers](#local-resolvers) section for more details concerning the resolver map). In the above example we added a `isInCart` resolver, for the `Launch` GraphQL object type, to the resolver map. Let's look at the `isInCart` resolver function more closely:
417+
Setting resolvers through the `LocalState`'s `resolvers` option or through its `addResolvers` method adds resolvers to the internal resolver map (refer to the [Local resolvers](#local-resolvers) section for more details concerning the resolver map). In the above example we added a `isInCart` resolver, for the `Launch` GraphQL object type, to the resolver map. Let's look at the `isInCart` resolver function more closely:
356418

357419
```js
358420
resolvers: {
359421
Launch: {
360-
isInCart: (launch, _args, { cache }) => {
361-
const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });
422+
isInCart: (launch, _args, { client }) => {
423+
const { cartItems } = client.cache.readQuery({ query: GET_CART_ITEMS });
362424
return cartItems.includes(launch.id);
363425
},
364426
},
365427
},
366428
```
367429

368-
`launch` holds the data returned from the server for the rest of the query, which means in this case we can use `launch` to get the current launch `id`. We aren't using any arguments in this resolver, so we can skip the second resolver parameter. From the `context` however (the third parameter), we're using the `cache` reference, to work directly with the cache ourselves. So in this resolver, we're making a call directly to the cache to get all cart items, checking to see if any of those loaded cart items matches the parent `launch.id`, and returning `true` / `false` accordingly. The returned boolean is then incorporated back into the result of running the original query.
430+
`launch` holds the data returned from the server for the rest of the query, which means in this case we can use `launch` to get the current launch `id`. We aren't using any arguments in this resolver, so we can skip the second resolver parameter. From the `context` however (the third parameter), we're using the `client` reference to access the cache. So in this resolver, we're making a call directly to the cache to get all cart items, checking to see if any of those loaded cart items matches the parent `launch.id`, and returning `true` / `false` accordingly. The returned boolean is then incorporated back into the result of running the original query.
369431

370432
Just like resolvers on the server, local resolvers are extremely flexible. They can be used to perform any kind of local computation you want, before returning a result for the specified field. You can manually query (or write to) the cache in different ways, call other helper utilities or libraries to prep/validate/clean data, track statistics, call into other data stores to prep a result, etc.
371433

@@ -954,7 +1016,7 @@ In order to toggle our todo, we need the todo and its status from the cache, whi
9541016
9551017
### Code splitting
9561018
957-
Depending on the complexity and size of your local resolvers, you might not always want to define them up front, when you create your initial `ApolloClient` instance. If you have local resolvers that are only needed in a specific part of your application, you can leverage Apollo Client's [`addResolvers` and `setResolvers`](#methods) functions to adjust your resolver map at any point. This can be really useful when leveraging techniques like route based code-splitting, using something like [`react-loadable`](https://github.com/jamiebuilds/react-loadable).
1019+
Depending on the complexity and size of your local resolvers, you might not always want to define them up front, when you create your initial `ApolloClient` instance. If you have local resolvers that are only needed in a specific part of your application, you can dynamically call `client.localState.addResolvers` to dynamically add new resolvers. This can be really useful when leveraging techniques like route based code-splitting.
9581020
9591021
Let's say we're building a messaging app and have a `/stats` route that is used to return the total number of messages stored locally. If we use `react-loadable` to load our `Stats` component like:
9601022
@@ -969,12 +1031,12 @@ export const Stats = Loadable({
9691031
});
9701032
```
9711033
972-
and wait until our `Stats` component is called to define our local resolvers (using `addResolvers`):
1034+
and wait until our `Stats` component is called to define our local resolvers:
9731035
9741036
```js
9751037
import React from "react";
9761038
import { gql } from "@apollo/client";
977-
import { ApolloConsumer, useApolloClient, useQuery } from "@apollo/client/react";
1039+
import { useApolloClient, useQuery } from "@apollo/client/react";
9781040

9791041
const GET_MESSAGE_COUNT = gql`
9801042
query GetMessageCount {
@@ -986,7 +1048,7 @@ const GET_MESSAGE_COUNT = gql`
9861048

9871049
const resolvers = {
9881050
Query: {
989-
messageCount: (_, args, { cache }) => {
1051+
messageCount: (_, args, { client }) => {
9901052
// ... calculate and return the number of messages in
9911053
// the cache ...
9921054
return {
@@ -999,7 +1061,7 @@ const resolvers = {
9991061

10001062
export function MessageCount() {
10011063
const client = useApolloClient();
1002-
client.addResolvers(resolvers);
1064+
client.localState.addResolvers(resolvers);
10031065

10041066
const { loading, data: { messageCount } } = useQuery(GET_MESSAGE_COUNT);
10051067

@@ -1040,15 +1102,19 @@ const client = new ApolloClient({
10401102
10411103
<FunctionDetails canonicalReference="@apollo/client!LocalState#addResolvers:member(1)" headingLevel={5} parameters={false} />
10421104
1043-
**Typescript interfaces/types:**
1105+
#### Typescript interfaces/types:
10441106
10451107
```ts
10461108
interface Resolvers {
10471109
[key: string]: {
10481110
[field: string]: (
10491111
rootValue?: any,
10501112
args?: any,
1051-
context?: any,
1113+
context?: {
1114+
requestContext: any;
1115+
client: ApolloClient;
1116+
phase: "exports" | "resolve";
1117+
},
10521118
info?: any,
10531119
) => any;
10541120
};

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@ The values for these fields are calculated locally using any logic you want, suc
1919

2020
As shown, a query can include both local-only fields _and_ fields that are fetched from your GraphQL server.
2121

22+
<Note>
23+
24+
**Important**: Using `@client` fields requires configuring local state in your Apollo Client instance. If you use `@client` fields without configuring local state, you'll get an error like:
25+
26+
```
27+
Query 'ProductDetails' contains '@client' fields but local state has not been configured.
28+
```
29+
30+
To enable `@client` fields, you must provide a `LocalState` instance to your Apollo Client:
31+
32+
```ts
33+
import { ApolloClient, InMemoryCache } from '@apollo/client';
34+
import { LocalState } from '@apollo/client/local-state';
35+
36+
const client = new ApolloClient({
37+
cache: new InMemoryCache(),
38+
localState: new LocalState(),
39+
});
40+
```
41+
42+
</Note>
43+
2244
## Defining
2345

2446
Let's say we're building an e-commerce application. Most of a product's details are stored on our back-end server, but we want to define a `Product.isInCart` boolean field that's local to the client. First, we create a **field policy** for `isInCart`.

0 commit comments

Comments
 (0)