You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/source/tutorial/local-state.md
+340-6Lines changed: 340 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,7 +5,7 @@ description: How to store and query local data in the Apollo cache
5
5
6
6
In almost every app we build, we display a combination of remote data from our graph API and local data such as network status, form state, and more. What's awesome about Apollo Client is that it allows us to store local data inside the Apollo cache and query it alongside our remote data with GraphQL.
7
7
8
-
We recommend managing local state in the Apollo cache instead of bringing in another state management library like Redux. The advantage of managing state this way is that the Apollo cache becomes the single source of truth for all data in our app and we don't have to synchronize our remote data with an external store.
8
+
We recommend managing local state in the Apollo cache instead of bringing in another state management library like Redux so the Apollo cache can be a single source of truth.
9
9
10
10
Managing local data with Apollo Client is very similar to how you've already managed remote data in this tutorial. You'll write a client schema and resolvers for your local data. You'll also learn to query it with GraphQL just by specifying the `@client` directive. Let's dive in!
11
11
@@ -20,7 +20,7 @@ _src/resolvers.js_
20
20
```js
21
21
importgqlfrom'graphql-tag';
22
22
23
-
exportconstschema=gql`
23
+
exportconsttypeDefs=gql`
24
24
extendtypeQuery {
25
25
isLoggedIn: Boolean!
26
26
cartItems: [Launch]!
@@ -31,7 +31,7 @@ export const schema = gql`
31
31
}
32
32
33
33
extendtypeMutation {
34
-
addOrRemoveFromCart: [Launch]
34
+
addOrRemoveFromCart(id: ID!): [Launch]
35
35
}
36
36
`;
37
37
```
@@ -64,12 +64,346 @@ const client = new ApolloClient({
64
64
});
65
65
```
66
66
67
-
These `storeInitializers` will be called as soon as `ApolloClient` is created. They will also run if the user resets the cache.
67
+
These `storeInitializers` will be called as soon as `ApolloClient` is created. They will also run if the store is reset.
68
68
69
-
Now that we've added default state to the Apollo cache, let's learn how we will query local data from within our React components.
69
+
Now that we've added default state to the Apollo cache, let's learn how to query local data from within our React components.
70
70
71
71
<h2id="local-query">Query local data</h2>
72
72
73
+
Querying local data from the Apollo cache is almost the same as querying remote data from a graph API. The only difference is that you add a `@client` directive to a local field to tell Apollo Client to pull it from the cache.
74
+
75
+
Let's look at an example where we query the `isLoggedIn` field we wrote to the cache in the last mutation exercise.
{({ data }) => (data.isLoggedIn?<Pages />:<Login />)}
97
+
</Query>
98
+
</ApolloProvider>,
99
+
document.getElementById('root'),
100
+
);
101
+
```
102
+
103
+
First, we create our `IsUserLoggedIn` local query by adding the `@client` directive to the `isLoggedIn` field. Then, we render a `Query` component, pass our local query in, and specify a render prop function that renders either a login screen or the homepage depending if the user is logged in. Since cache reads are synchronous, we don't have to account for any loading state.
104
+
105
+
Let's look at another example of a component that queries local state in `src/pages/cart.js`. Just like before, we create our query:
106
+
107
+
_src/pages/cart.js_
108
+
109
+
```js
110
+
importReact, { Fragment } from'react';
111
+
import { Query } from'react-apollo';
112
+
importgqlfrom'graphql-tag';
113
+
114
+
importHeaderfrom'../components/header';
115
+
importLoadingfrom'../components/loading';
116
+
importCartItemfrom'../containers/cart-item';
117
+
importBookTripsfrom'../containers/book-trips';
118
+
119
+
exportconstGET_CART_ITEMS=gql`
120
+
queryGetCartItems {
121
+
cartItems@client
122
+
}
123
+
`;
124
+
```
125
+
126
+
Next, we render our `Query` component and bind it to our `GetCartItems` query:
127
+
128
+
_src/pages/cart.js_
129
+
130
+
```js
131
+
exportdefaultfunctionCart() {
132
+
return (
133
+
<Query query={GET_CART_ITEMS}>
134
+
{({ data, loading, error }) => {
135
+
if (loading) return<Loading />;
136
+
if (error) return<p>ERROR: {error.message}</p>;
137
+
return (
138
+
<Fragment>
139
+
<Header>My Cart</Header>
140
+
{!data.cartItems||!data.cartItems.length? (
141
+
<p>No items in your cart</p>
142
+
) : (
143
+
<Fragment>
144
+
{data.cartItems.map(launchId=> (
145
+
<CartItem key={launchId} launchId={launchId} />
146
+
))}
147
+
<BookTrips cartItems={data.cartItems} />
148
+
</Fragment>
149
+
)}
150
+
</Fragment>
151
+
);
152
+
}}
153
+
</Query>
154
+
);
155
+
}
156
+
```
157
+
158
+
It's important to note that you can mix local queries with remote queries in a single GraphQL document. Now that you're a pro at querying local data with GraphQL, let's learn how to add local fields to server data.
159
+
160
+
<h3id="virtual-fields">Adding virtual fields to server data</h3>
161
+
162
+
One of the unique advantages of managing your local data with Apollo Client is that you can add **virtual fields** to data you receive back from your graph API. These fields only exist on the client and are useful for decorating server data with local state. In our example, we're going to add an `isInCart` virtual field to our `Launch` type.
163
+
164
+
To add a virtual field, first extend the type of the data you're adding the field to in your client schema. Here, we're extending the `Launch` type:
165
+
166
+
_src/resolvers.js_
167
+
168
+
```js
169
+
importgqlfrom'graphql-tag';
170
+
171
+
exportconstschema=gql`
172
+
extendtypeLaunch {
173
+
isInCart: Boolean!
174
+
}
175
+
`;
176
+
```
177
+
178
+
Next, specify a client resolver on the `Launch` type to tell Apollo Client how to resolve your virtual field:
We're going to learn more about client resolvers in the section below. The important thing to note is that the resolver API on the client is the same as the resolver API on the server.
194
+
195
+
Now, you're ready to query your virtual field on the launch detail page! Similar to the previous examples, just add your virtual field to a query and specify the `@client` directive.
196
+
197
+
_src/pages/launch.js_
198
+
199
+
```js lines=4
200
+
exportconstGET_LAUNCH_DETAILS=gql`
201
+
queryLaunchDetails($launchId: ID!) {
202
+
launch(id: $launchId) {
203
+
isInCart@client
204
+
site
205
+
rocket {
206
+
type
207
+
}
208
+
...LaunchTile
209
+
}
210
+
}
211
+
${LAUNCH_TILE_DATA}
212
+
`;
213
+
```
214
+
73
215
<h2id="local-mutation">Update local data</h2>
74
216
75
-
<h2id="cart">Build a cart</h2>
217
+
Up until now, we've focused on querying local data from the Apollo cache. Apollo Client also lets you update local data in the cache with either **direct cache writes** or **client resolvers**. Direct writes are typically used to write simple booleans or strings to the cache whereas client resolvers are for more complicated writes such as adding or removing data from a list.
218
+
219
+
<h3id="direct-writes">Direct cache writes</h3>
220
+
221
+
Direct cache writes are convenient when you want to write a simple field, like a boolean or a string, to the Apollo cache. We perform a direct write by calling `client.writeData()` and passing in an object with a data property that corresponds to the data we want to write to the cache. We've already seen an example of a direct write when we called `client.writeData` in the `onCompleted` handler for the login `Mutation` component. Let's look at a similar example where we copy the code below to create a logout button:
When we click the button, we perform a direct cache write by calling `client.writeData` and passing in a data object that sets the `isLoggedIn` boolean to false.
253
+
254
+
We can also perform direct writes within the `update` function of a `Mutation` component. The `update` function allows us to manually update the cache after a mutation occurs without refetching data. Let's look at an example in `src/containers/book-trips.js`:
255
+
256
+
_src/containers/book-trips.js_
257
+
258
+
```js lines=30-32
259
+
importReactfrom'react';
260
+
import { Mutation } from'react-apollo';
261
+
importgqlfrom'graphql-tag';
262
+
263
+
importButtonfrom'../components/button';
264
+
import { GET_LAUNCH } from'./cart-item';
265
+
266
+
constBOOK_TRIPS=gql`
267
+
mutationBookTrips($launchIds: [ID]!) {
268
+
bookTrips(launchIds: $launchIds) {
269
+
success
270
+
message
271
+
launches {
272
+
id
273
+
isBooked
274
+
}
275
+
}
276
+
}
277
+
`;
278
+
279
+
exportdefaultfunctionBookTrips({ cartItems }) {
280
+
return (
281
+
<Mutation
282
+
mutation={BOOK_TRIPS}
283
+
variables={{ launchIds: cartItems }}
284
+
refetchQueries={cartItems.map(launchId=> ({
285
+
query:GET_LAUNCH,
286
+
variables: { launchId },
287
+
}))}
288
+
update={cache=> {
289
+
cache.writeData({ data: { cartItems: [] } });
290
+
}}
291
+
>
292
+
{(bookTrips, { data, loading, error }) =>
293
+
data &&data.bookTrips&&!data.bookTrips.success? (
294
+
<p>{data.bookTrips.message}</p>
295
+
) : (
296
+
<Button onClick={bookTrips}>Book All</Button>
297
+
)
298
+
}
299
+
</Mutation>
300
+
);
301
+
}
302
+
```
303
+
304
+
In this example, we're directly calling `cache.writeData` to reset the state of the `cartItems` after the `BookTrips` mutation is sent to the server. This direct write is performed inside of the update function, which is passed our Apollo Client instance.
305
+
306
+
<h3id="resolvers">Local resolvers</h3>
307
+
308
+
We're not done yet! What if we wanted to perform a more complicated local data update such as adding or removing items from a list? For this situation, we'll use a local resolver. Local resolvers have the same function signature as remote resolvers (`(parent, args, context, info) => data`). The only difference is that the Apollo cache is already added to the context for you. Inside your resolver, you'll use the cache to read and write data.
309
+
310
+
Let's write the local resolver for the `addOrRemoveFromCart` mutation. You should place this resolver underneath the `Launch` resolver we wrote earlier.
In this resolver, we destructure the Apollo `cache` from the context in order to read the query that fetches cart items. Once we have our cart data, we either remove or add the cart item's `id` passed into the mutation to the list. Finally, we return the updated list from the mutation.
332
+
333
+
Let's see how we call the `addOrRemoveFromCart` mutation in a component:
334
+
335
+
_src/containers/action-button.js_
336
+
337
+
```js
338
+
importgqlfrom'graphql-tag';
339
+
340
+
constTOGGLE_CART=gql`
341
+
mutationaddOrRemoveFromCart($launchId: ID!) {
342
+
addOrRemoveFromCart(id: $launchId) @client
343
+
}
344
+
`;
345
+
```
346
+
347
+
Just like before, the only thing we need to add to our mutation is a `@client` directive to tell Apollo to resolve this mutation from the cache instead of a remote server.
348
+
349
+
Now that our local mutation is complete, let's build out the rest of the `ActionButton` component so we can finish building the cart:
In this example, we're using the `isBooked` prop passed into the component to determine which mutation we should fire. Just like remote mutations, we can pass in our local mutations to the same `Mutation` component.
405
+
406
+
___
407
+
408
+
Congratulations! 🎉 You've officially made it to the end of the Apollo platform tutorial. In the final section, we're going to recap what we just learned and give you guidance on what you should learn next.
0 commit comments