Skip to content

Commit d84d555

Browse files
committed
Finalized local state section
1 parent 829c764 commit d84d555

2 files changed

Lines changed: 401 additions & 7 deletions

File tree

docs/source/tutorial/local-state.md

Lines changed: 340 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: How to store and query local data in the Apollo cache
55

66
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.
77

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.
99

1010
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!
1111

@@ -20,7 +20,7 @@ _src/resolvers.js_
2020
```js
2121
import gql from 'graphql-tag';
2222

23-
export const schema = gql`
23+
export const typeDefs = gql`
2424
extend type Query {
2525
isLoggedIn: Boolean!
2626
cartItems: [Launch]!
@@ -31,7 +31,7 @@ export const schema = gql`
3131
}
3232
3333
extend type Mutation {
34-
addOrRemoveFromCart: [Launch]
34+
addOrRemoveFromCart(id: ID!): [Launch]
3535
}
3636
`;
3737
```
@@ -64,12 +64,346 @@ const client = new ApolloClient({
6464
});
6565
```
6666

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.
6868

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.
7070

7171
<h2 id="local-query">Query local data</h2>
7272

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.
76+
77+
_src/index.js_
78+
79+
```js lines=6,13-15
80+
import { Query, ApolloProvider } from 'react-apollo';
81+
import gql from 'graphql-tag';
82+
83+
import Pages from './pages';
84+
import Login from './pages/login';
85+
86+
const IS_LOGGED_IN = gql`
87+
query IsUserLoggedIn {
88+
isLoggedIn @client
89+
}
90+
`;
91+
92+
injectStyles();
93+
ReactDOM.render(
94+
<ApolloProvider client={client}>
95+
<Query query={IS_LOGGED_IN}>
96+
{({ 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+
import React, { Fragment } from 'react';
111+
import { Query } from 'react-apollo';
112+
import gql from 'graphql-tag';
113+
114+
import Header from '../components/header';
115+
import Loading from '../components/loading';
116+
import CartItem from '../containers/cart-item';
117+
import BookTrips from '../containers/book-trips';
118+
119+
export const GET_CART_ITEMS = gql`
120+
query GetCartItems {
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+
export default function Cart() {
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+
<h3 id="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+
import gql from 'graphql-tag';
170+
171+
export const schema = gql`
172+
extend type Launch {
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:
179+
180+
_src/resolvers.js_
181+
182+
```js
183+
export const resolvers = {
184+
Launch: {
185+
isInCart: (launch, _, { cache }) => {
186+
const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });
187+
return cartItems.includes(launch.id);
188+
},
189+
}
190+
};
191+
```
192+
193+
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+
export const GET_LAUNCH_DETAILS = gql`
201+
query LaunchDetails($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+
73215
<h2 id="local-mutation">Update local data</h2>
74216

75-
<h2 id="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+
<h3 id="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:
222+
223+
_src/containers/logout-button.js_
224+
225+
```js lines=14
226+
import React from 'react';
227+
import styled from 'react-emotion';
228+
import { ApolloConsumer } from 'react-apollo';
229+
230+
import { menuItemClassName } from '../components/menu-item';
231+
import { ReactComponent as ExitIcon } from '../assets/icons/exit.svg';
232+
233+
export default function LogoutButton() {
234+
return (
235+
<ApolloConsumer>
236+
{client => (
237+
<StyledButton
238+
onClick={() => {
239+
client.writeData({ data: { isLoggedIn: false } });
240+
localStorage.clear();
241+
}}
242+
>
243+
<ExitIcon />
244+
Logout
245+
</StyledButton>
246+
)}
247+
</ApolloConsumer>
248+
);
249+
}
250+
```
251+
252+
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+
import React from 'react';
260+
import { Mutation } from 'react-apollo';
261+
import gql from 'graphql-tag';
262+
263+
import Button from '../components/button';
264+
import { GET_LAUNCH } from './cart-item';
265+
266+
const BOOK_TRIPS = gql`
267+
mutation BookTrips($launchIds: [ID]!) {
268+
bookTrips(launchIds: $launchIds) {
269+
success
270+
message
271+
launches {
272+
id
273+
isBooked
274+
}
275+
}
276+
}
277+
`;
278+
279+
export default function BookTrips({ 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+
<h3 id="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.
311+
312+
_src/resolvers.js_
313+
314+
```js
315+
export const resolvers = {
316+
Mutation: {
317+
addOrRemoveFromCart: (_, { id }, { cache }) => {
318+
const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });
319+
const data = {
320+
cartItems: cartItems.includes(id)
321+
? cartItems.filter(i => !i)
322+
: [...cartItems, id],
323+
};
324+
cache.writeQuery({ query: GET_CART_ITEMS, data });
325+
return data.cartItems;
326+
},
327+
},
328+
};
329+
```
330+
331+
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+
import gql from 'graphql-tag';
339+
340+
const TOGGLE_CART = gql`
341+
mutation addOrRemoveFromCart($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:
350+
351+
_src/containers/action-button.js_
352+
353+
```js
354+
import React from 'react';
355+
import { Mutation } from 'react-apollo';
356+
import gql from 'graphql-tag';
357+
358+
import { GET_LAUNCH_DETAILS } from '../pages/launch';
359+
import Button from '../components/button';
360+
361+
const CANCEL_TRIP = gql`
362+
mutation cancel($launchId: ID!) {
363+
cancelTrip(launchId: $launchId) {
364+
success
365+
message
366+
launches {
367+
id
368+
isBooked
369+
}
370+
}
371+
}
372+
`;
373+
374+
export default function ActionButton({ isBooked, id, isInCart }) {
375+
return (
376+
<Mutation
377+
mutation={isBooked ? CANCEL_TRIP : TOGGLE_CART}
378+
variables={{ launchId: id }}
379+
refetchQueries={[
380+
{
381+
query: GET_LAUNCH_DETAILS,
382+
variables: { launchId: id },
383+
},
384+
]}
385+
>
386+
{(mutate, { data, loading, error }) => {
387+
return (
388+
<div>
389+
<Button onClick={mutate} isBooked={isBooked}>
390+
{isBooked
391+
? 'Cancel This Trip'
392+
: isInCart
393+
? 'Remove from Cart'
394+
: 'Add to Cart'}
395+
</Button>
396+
</div>
397+
);
398+
}}
399+
</Mutation>
400+
);
401+
}
402+
```
403+
404+
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.
409+

0 commit comments

Comments
 (0)