Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1---
2title: Cache Updates
3order: 4
4---
5
6# Cache Updates
7
8As we've learned [on the page on "Normalized
9Caching"](./normalized-caching.md#normalizing-relational-data), when Graphcache receives an API
10result it will traverse and store all its data to its cache in a normalized structure. Each entity
11that is found in a result will be stored under the entity's key.
12
13A query's result is represented as a graph, which can also be understood as a tree structure,
14starting from the root `Query` entity, which then connects to other entities via links, which are
15relations stored as keys, where each entity has records that store scalar values, which are the
16tree's leafs. On the previous page, on ["Local Resolvers"](./local-resolvers.md), we've seen how
17resolvers can be attached to fields to manually resolve other entities (or transform record fields).
18Local Resolvers passively _compute_ results and change how Graphcache traverses and sees its locally
19cached data, however, for **mutations** and **subscriptions** we cannot passively compute data.
20
21When Graphcache receives a mutation or subscription result it still traverses it using the query
22document as we've learned when reading about how Graphcache stores normalized data,
23[quote](./normalized-caching.md/#storing-normalized-data):
24
25> Any mutation or subscription can also be written to this data structure. Once Graphcache finds a
26> keyable entity in their results it's written to its relational table, which may update other
27> queries in our application.
28
29This means that mutations and subscriptions still write and update entities in the cache. These
30updates are then reflected on all active queries that our app uses. However, there are limitations to this.
31While resolvers can be used to passively change data for queries, for mutations
32and subscriptions we sometimes have to write **updaters** to update links and relations.
33This is often necessary when a given mutation or subscription deliver a result that is more granular
34than the cache needs to update all affected entities.
35
36Previously, we've learned about cache updates [on the "Normalized Caching"
37page](./normalized-caching.md#manual-cache-updates).
38
39The `updates` option on `cacheExchange` accepts a map for `Mutation` or `Subscription` keys on which
40we can add "updater functions" to react to mutation or subscription results. These `updates`
41functions look similar to ["Local Resolvers"](./local-resolvers.md) that we've seen in the last
42section and similar to [GraphQL.js' resolvers on the
43server-side](https://www.graphql-tools.com/docs/resolvers/).
44
45```js
46cacheExchange({
47 updates: {
48 Mutation: {
49 mutationField: (result, args, cache, info) => {
50 // ...
51 },
52 },
53 Subscription: {
54 subscriptionField: (result, args, cache, info) => {
55 // ...
56 },
57 },
58 },
59});
60```
61
62## Default mutation invalidation
63
64Starting in [Graphcache v7](https://github.com/urql-graphql/urql/blob/main/exchanges/graphcache/CHANGELOG.md#700),
65mutations without a configured `updates.Mutation.<fieldName>` updater have a fallback behavior:
66
67- If the mutation returns an entity that can't be found in the cache yet, Graphcache treats this as
68 a "create" mutation.
69- Graphcache then invalidates cached entities of the same `__typename` as the one returned from the mutation, which can trigger related queries to refetch.
70
71As soon as you define an updater for that mutation field, this fallback behavior no longer runs and
72your updater fully controls what happens after the mutation write.
73
74An "updater" may be attached to a `Mutation` or `Subscription` field and accepts four positional
75arguments, which are the same as [the resolvers' arguments](./local-resolvers.md):
76
77- `result`: The full API result that's being written to the cache. Typically we'd want to
78 avoid coupling by only looking at the current field that the updater is attached to, but it's
79 worth noting that we can access any part of the result.
80- `args`: The arguments that the field has been called with, which will be replaced with an empty
81 object if the field hasn't been called with any arguments.
82- `cache`: The `cache` instance, which gives us access to methods allowing us to interact with the
83 local cache. Its full API can be found [in the API docs](../api/graphcache.md#cache). On this page
84 we use it frequently to read from and write to the cache.
85- `info`: This argument shouldn't be used frequently, but it contains running information about the
86 traversal of the query document. It allows us to make resolvers reusable or to retrieve
87 information about the entire query. Its full API can be found [in the API
88 docs](../api/graphcache.md#info).
89
90The cache updaters return value is disregarded (and typed as `void` in TypeScript), which makes any
91method that they call on the `cache` instance a side effect, which may trigger additional cache
92changes and updates all affected queries as we modify them.
93
94## Why do we need cache updates?
95
96When we’re designing a GraphQL schema well, we won’t need to write many cache updaters for
97Graphcache.
98
99For example, we may have a mutation to update a username on a `User`, which can trivially
100update the cache without us writing an updater because it resolves the `User`.
101
102```graphql
103query User($id: ID!) {
104 user(id: $id) {
105 __typename # "User"
106 id
107 username
108 }
109}
110
111mutation UpdateUsername($id: ID!, $username: String!) {
112 updateUser(id: $id, username: $username) {
113 __typename # "User"
114 id
115 username
116 }
117}
118```
119
120In the above example, `Query.user` returns a `User`, which is then updated by a mutation on
121`Mutation.updateUser`. Since the mutation also queries the `User`, the updated username will
122automatically be applied by Graphcache. If the mutation field didn’t return a `User`, then this
123wouldn’t be possible, and while we can write an updater in Graphcache for it, we should consider
124this poor schema design.
125
126An updater instead becomes absolutely necessary when a mutation can’t reasonably return what has
127changed or when we can’t manually define a selection set that’d be even able to select all fields
128that may update. Some examples may include:
129
130- `Mutation.deleteUser`, since we’ll need to invalidate an entity
131- `Mutation.createUser`, since a list may now have to include a new entity
132- `Mutation.createBook`, since a given entity, e.g. `User` may have a field `User.books` that now
133 needs to be updated.
134
135In short, we may need to write a cache updater for any **relation** (i.e. link) that we can’t query
136via our GraphQL mutation directly, since there’ll be changes to our data that Graphcache won’t be
137able to see and store.
138
139In a later section on this page, [we’ll learn about the `cache.link` method.](#writing-links-individually)
140This method is used to update a field to point at a different entity. In other words, `cache.link`
141is used to update a relation from one entity field to one or more other child entities.
142This is the most common update we’ll need and it’s preferable to always try to use `cache.link`,
143unless we need to update a scalar.
144
145## Manually updating entities
146
147If a mutation field's result isn't returning the full entity it updates then it becomes impossible
148for Graphcache to update said entity automatically. For instance, we may have a mutation like the
149following:
150
151```graphql
152mutation UpdateTodo($todoId: ID!, $date: String!) {
153 updateTodoDate(id: $todoId, date: $date)
154}
155```
156
157In this hypothetical case instead of `Mutation.updateDate` resolving to the full `Todo` object type
158it instead results in a scalar. This could be fixed by changing the `Mutation` in our API's schema
159to instead return the full `Todo` entity, which would allow us to run the mutation as such, which
160updates the `Todo` in our cache automatically:
161
162```graphql
163mutation UpdateTodo($todoId: ID!, $date: String!) {
164 updateTodoDate(id: $todoId, date: $date) {
165 ...Todo_date
166 }
167}
168
169fragment Todo_date on Todo {
170 id
171 updatedAt
172}
173```
174
175However, if this isn't possible we can instead write an updater that updates our `Todo` entity
176manually by using the `cache.writeFragment` method:
177
178```js
179import { gql } from '@urql/core';
180
181cacheExchange({
182 updates: {
183 Mutation: {
184 updateTodoDate(_result, args, cache, _info) {
185 const fragment = gql`
186 fragment _ on Todo {
187 id
188 updatedAt
189 }
190 `;
191
192 cache.writeFragment(fragment, { id: args.id, updatedAt: args.date });
193 },
194 },
195 },
196});
197```
198
199The `cache.writeFragment` method is similar to the `cache.readFragment` method that we've seen [on
200the "Local Resolvers" page before](./local-resolvers.md#reading-a-fragment). Instead of reading data
201for a given fragment it instead writes data to the cache.
202
203> **Note:** In the above example, we've used
204> [the `gql` tag function](../api/core.md#gql) because `writeFragment` only accepts
205> GraphQL `DocumentNode`s as inputs, and not strings.
206
207### Cache Updates outside updaters
208
209Cache updates are **not** possible outside `updates`'s functions. If we attempt to store the `cache`
210in a variable and call its methods outside any `updates` functions (or functions, like `resolvers`)
211then Graphcache will throw an error.
212
213Methods like these cannot be called outside the `cacheExchange`'s `updates` functions, because
214all updates are isolated to be _reactive_ to mutations and subscription events. In Graphcache,
215out-of-band updates aren't permitted because the cache attempts to only represent the server's
216state. This limitation keeps the data of the cache true to the server data we receive from API
217results and makes its behaviour much more predictable.
218
219If we still manage to call any of the cache's methods outside its callbacks in its configuration,
220we will receive [a "(2) Invalid Cache Call" error](./errors.md#2-invalid-cache-call).
221
222### Updaters on arbitrary types
223
224Cache updates **may** be configured for arbitrary types and not just for `Mutation` or
225`Subscription` fields. However, this can potentially be **dangerous** and is an easy trap
226to fall into. It is allowed though because it allows for some nice tricks and workarounds.
227
228Given an updater on an arbitrary type, e.g. `Todo.author`, we can chain updates onto this field
229whenever it’s written. The updater can then be triggerd by Graphcache during _any_ operation;
230mutations, queries, and subscriptions. When this update is triggered, it allows us to add more
231arbitrary updates onto this field.
232
233> **Note:** If you’re looking to use this because you’re nesting mutations onto other object types,
234> e.g. `Mutation.author.updateName`, please consider changing your schema first before using this.
235> Namespacing mutations is not recommended and changes the execution order to be concurrent rather
236> than sequential when you use multiple nested mutation fields.
237
238## Updating lists or links
239
240Mutations that create new entities are pretty common, and it's not uncommon to attempt to update the
241cache when a mutation result for these "creation" mutations come back, since this avoids an
242additional roundtrip to our APIs.
243
244While it's possible for these mutations to return any affected entities that carry the lists as
245well, often these lists live on fields on or below the `Query` root type, which means that we'd be
246sending a rather large API result. For large amounts of pages this is especially infeasible.
247Instead, most schemas opt to instead just return the entity that's just been created:
248
249```graphql
250mutation NewTodo($text: String!) {
251 createTodo(id: $todoId, text: $text) {
252 id
253 text
254 }
255}
256```
257
258If we have a corresponding field on `Query.todos` that contains all of our `Todo` entities then this
259means that we'll need to create an updater that automatically adds the `Todo` to our list:
260
261```js
262cacheExchange({
263 updates: {
264 Mutation: {
265 createTodo(result, _args, cache, _info) {
266 const TodoList = gql`
267 {
268 todos {
269 id
270 }
271 }
272 `;
273
274 cache.updateQuery({ query: TodoList }, data => {
275 return {
276 ...data,
277 todos: [...data.todos, result.createTodo],
278 };
279 });
280 },
281 },
282 },
283});
284```
285
286Here we use the `cache.updateQuery` method, which is similar to the [`cache.readQuery` method](./local-resolvers.md#reading-a-query) that
287we've seen on the "Local Resolvers" page before.
288
289This method accepts a callback, which will give us the `data` of the query, as read from the locally
290cached data, and we may return an updated version of this data. While we may want to instinctively
291opt for immutably copying and modifying this data, we're actually allowed to mutate it directly,
292since it's just a copy of the data that's been read by the cache.
293
294This `data` may also be `null` if the cache doesn't actually have enough locally cached information
295to fulfil the query. This is important because resolvers aren't actually applied to cache methods in
296updaters. All resolvers are ignored, so it becomes impossible to accidentally commit transformed data
297to our cache. We could safely add a resolver for `Todo.createdAt` and wouldn't have to worry about
298an updater accidentally writing it to the cache's internal data structure.
299
300### Writing links individually
301
302As long as we're only updating links (as in 'relations') then we may also use the [`cache.link`
303method](../api/graphcache.md#link). This method is the "write equivalent" of [the `cache.resolve`
304method, as seen on the "Local Resolvers" page before.](./local-resolvers.md#resolving-other-fields)
305
306We can use this method to update any relation in our cache, so the example above could also be
307rewritten to use `cache.link` and `cache.resolve` rather than `cache.updateQuery`.
308
309```js
310cacheExchange({
311 updates: {
312 Mutation: {
313 createTodo(result, _args, cache, _info) {
314 const todos = cache.resolve('Query', 'todos');
315 if (Array.isArray(todos)) {
316 cache.link('Query', 'todos', [...todos, result.createTodo]);
317 }
318 },
319 },
320 },
321});
322```
323
324This method can be combined with more than just `cache.resolve`, for instance, it's a good fit with
325`cache.inspectFields`. However, when you're writing records (as in 'scalar' values)
326`cache.writeFragment` and `cache.updateQuery` are still the only methods that you can use.
327But since this kind of data is often written automatically by the normalized cache, often updating a
328link is the only modification we may want to make.
329
330## Updating many unknown links
331
332In the previous section we've seen how to update data, like a list, when a mutation result enters
333the cache. However, we've used a rather simple example when we've looked at a single list on a known
334field.
335
336In many schemas pagination is quite common, and when we for instance delete a todo then knowing the
337lists to update becomes unknowable. We cannot know ahead of time how many pages (and its variables)
338we've already accessed. This knowledge in fact _shouldn't_ be available to Graphcache. Querying the
339`Client` is an entirely separate concern that's often colocated with some part of our
340UI code.
341
342```graphql
343mutation RemoveTodo($id: ID!) {
344 removeTodo(id: $id)
345}
346```
347
348Suppose we have the above mutation, which deletes a `Todo` entity by its ID. Our app may query a list
349of these items over many pages with separate queries being sent to our API, which makes it hard to
350know the fields that should be checked:
351
352```graphql
353query PaginatedTodos($skip: Int) {
354 todos(skip: $skip) {
355 id
356 text
357 }
358}
359```
360
361Instead, we can **introspect an entity's fields** to find the fields we may want to update
362dynamically. This is possible thanks to [the `cache.inspectFields`
363method](../api/graphcache.md#inspectfields). This method accepts a key, or a keyable entity like the
364`cache.keyOfEntity` method that [we've seen on the "Local Resolvers"
365page](./local-resolvers.md#resolving-by-keys) or the `cache.resolve` method's first argument.
366
367```js
368cacheExchange({
369 updates: {
370 Mutation: {
371 removeTodo(_result, args, cache, _info) {
372 const TodoList = gql`
373 query (skip: $skip) {
374 todos(skip: $skip) { id }
375 }
376 `;
377
378 const fields = cache
379 .inspectFields('Query')
380 .filter(field => field.fieldName === 'todos')
381 .forEach(field => {
382 cache.updateQuery(
383 {
384 query: TodoList,
385 variables: { skip: field.arguments.skip },
386 },
387 data => {
388 data.todos = data.todos.filter(todo => todo.id !== args.id);
389 return data;
390 }
391 );
392 });
393 },
394 },
395 },
396});
397```
398
399To implement an updater for our example's `removeTodo` mutation field we may use the
400`cache.inspectFields('Query')` method to retrieve a list of all fields on the `Query` root entity.
401This list will contain all known fields on the `"Query"` entity. Each field is described as an
402object with three properties:
403
404- `fieldName`: The field's name; in this case we're filtering for all `todos` listing fields.
405- `arguments`: The arguments for the given field, since each field that accepts arguments can be
406 accessed multiple times with different arguments. In this example we're looking at
407 `arguments.skip` to find all unique pages.
408- `fieldKey`: This is the field's key, which can come in useful to retrieve a field using
409 `cache.resolve(entityKey, fieldKey)` to prevent the arguments from having to be stringified
410 repeatedly.
411
412To summarise, we filter the list of fields in our example down to only the `todos` fields and
413iterate over each of our `arguments` for the `todos` field to filter all lists to remove the `Todo`
414from them.
415
416### Inspecting arbitrary entities
417
418We're not required to only inspecting fields on the `Query` root entity. Instead, we can inspect
419fields on any entity by passing a different partial, keyable entity or key to `cache.inspectFields`.
420
421For instance, if we had a `Todo` entity and wanted to get all of its known fields then we could pass
422in a partial `Todo` entity just as well:
423
424```js
425cache.inspectFields({
426 __typename: 'Todo',
427 id: args.id,
428});
429```
430
431## Invalidating Entities
432
433Admittedly, it's sometimes almost impossible to write updaters for all mutations. It's often even
434hard to predict what our APIs may do when they receive a mutation. An update of an entity may change
435the sorting of a list, or remove an item from a list in a way we can't predict, since we don't have
436access to a full database to run the API locally.
437
438In cases like these it may be advisable to trigger a refetch instead and let the cache update itself
439by sending queries that have invalidated data associated to them to our API again. This process is
440called **invalidation** since it removes data from Graphcache's locally cached data.
441
442We may use the cache's [`cache.invalidate` method](../api/graphcache.md#invalidate) to either
443invalidate entire entities or individual fields. It has the same signature as [the `cache.resolve`
444method](../api/graphcache.md#resolve), which we've already seen [on the "Local Resolvers" page as
445well](./local-resolvers.md#resolving-other-fields). We can simplify the previous update we've written
446with a call to `cache.invalidate`:
447
448```js
449cacheExchange({
450 updates: {
451 Mutation: {
452 removeTodo(_result, args, cache, _info) {
453 cache.invalidate({
454 __typename: 'Todo',
455 id: args.id,
456 });
457 },
458 },
459 },
460});
461```
462
463Like any other cache update, this will cause all queries that use this `Todo` entity to be updated
464against the cache. Since we've invalidated the `Todo` item they're using these queries will be
465refetched and sent to our API.
466
467If we're using ["Schema Awareness"](./schema-awareness.md) then these queries' results may actually
468be temporarily updated with a partial result, but in general we should observe that queries with
469data that has been invalidated will be refetched as some of their data isn't cached anymore.
470
471### Invalidating individual fields
472
473We may also want to only invalidate individual fields, since maybe not all queries have to be
474immediately updated. We can pass a field (and optional arguments) to the `cache.invalidate` method
475as well to only invalidate a single field.
476
477For instance, we can use this to invalidate our lists instead of invalidating the entity itself.
478This can be useful if we know that modifying an entity will cause our list to be sorted differently,
479for instance.
480
481```js
482cacheExchange({
483 updates: {
484 Mutation: {
485 updateTodo(_result, args, cache, _info) {
486 const key = 'Query';
487 const fields = cache
488 .inspectFields(key)
489 .filter(field => field.fieldName === 'todos')
490 .forEach(field => {
491 cache.invalidate(key, field.fieldKey);
492 // or alternatively:
493 cache.invalidate(key, field.fieldName, field.arguments);
494 });
495 },
496 },
497 },
498});
499```
500
501In this example we've attached an updater to a `Mutation.updateTodo` field. We react to this
502mutation by enumerating all `todos` listing fields using `cache.inspectFields` and targetedly
503invalidate only these fields, which causes all queries using these listing fields to be refetched.
504
505### Invalidating a type
506
507We can also invalidate all the entities of a given type, this could be handy in the case of a
508list update or when you aren't sure what entity is affected.
509
510This can be done by only passing the relevant `__typename` to the `invalidate` function.
511
512```js
513cacheExchange({
514 updates: {
515 Mutation: {
516 deleteTodo(_result, args, cache, _info) {
517 cache.invalidate('Todo');
518 },
519 },
520 },
521});
522```
523
524## Optimistic updates
525
526If we know what result a mutation may return, why wait for the GraphQL API to fulfill our mutations?
527
528In addition to the `updates` configuration, we can also pass an `optimistic` option to the
529`cacheExchange`. This option is a factory function that allows us to create a "virtual" result for a
530mutation. This temporary result can be applied immediately to the cache to give our users the
531illusion that mutations were executed immediately, which is a great method to reduce waiting time
532and to make our apps feel snappier.
533This technique is often used with one-off mutations that are assumed to succeed, like starring a
534repository, or liking a tweet. In such cases it's often desirable to make the interaction feel
535as instant as possible.
536
537The `optimistic` configuration is similar to our `resolvers` or `updates` configuration, except that
538it only receives a single map for mutation fields. We can attach optimistic functions to any
539mutation field to make it generate an optimistic that is applied to the cache while the `Client`
540waits for a response from our API. An "optimistic" function accepts three positional arguments,
541which are the same as the resolvers' or updaters' arguments, except for the first one:
542
543The `optimistic` functions receive the same arguments as `updates` functions, except for `parent`,
544since we don't have any server data to work with:
545
546- `args`: The arguments that the field has been called with, which will be replaced with an empty
547 object if the field hasn't been called with any arguments.
548- `cache`: The `cache` instance, which gives us access to methods allowing us to interact with the
549 local cache. Its full API can be found [in the API docs](../api/graphcache.md#cache). On this page
550 we use it frequently to read from and write to the cache.
551- `info`: This argument shouldn't be used frequently, but it contains running information about the
552 traversal of the query document. It allows us to make resolvers reusable or to retrieve
553 information about the entire query. Its full API can be found [in the API
554 docs](../api/graphcache.md#info).
555
556The usual `parent` argument isn't present since optimistic functions don't have any server data to
557handle or deal with and instead create this data. When a mutation is run that contains one or more
558optimistic mutation fields, Graphcache picks these up and generates immediate changes, which it
559applies to the cache. The `resolvers` functions also trigger as if the results were real server
560results.
561
562This modification is temporary. Once a result from the API comes back it's reverted, which leaves us
563in a state where the cache can apply the "real" result to the cache.
564
565> Note: While optimistic mutations are waiting for results from the API all queries that may alter
566> our optimistic data are paused (or rather queued up) and all optimistic mutations will be reverted
567> at the same time. This means that optimistic results can stack but will never accidentally be
568> confused with "real" data in your configuration.
569
570In the following example we assume that we'd like to implement an optimistic result for a
571`favoriteTodo` mutation, like such:
572
573```graphql
574mutation FavoriteTodo(id: $id) {
575 favoriteTodo(id: $id) {
576 id
577 favorite
578 updatedAt
579 }
580}
581```
582
583The mutation is rather simple and all we have to do is create a function
584that imitates the result that the API is assumed to send back:
585
586```js
587const cache = cacheExchange({
588 optimistic: {
589 favoriteTodo(args, cache, info) {
590 return {
591 __typename: 'Todo',
592 id: args.id,
593 favorite: true,
594 };
595 },
596 },
597});
598```
599
600This optimistic mutation will be applied to the cache. If any `updates` configuration exists for
601`Mutation.favoriteTodo` then it will be executed using the optimistic result.
602Once the mutation result comes back from our API this temporary change will be rolled back and
603discarded.
604
605In the above example optimistic mutation function we also see that `updatedAt` is not present in our
606optimistic return value. That’s because we don’t always have to (or can) match our mutations’
607selection sets perfectly. Instead, Graphcache will skip over fields and use cached fields for any we
608leave out. This can even work on nested entities and fields.
609
610However, leaving out fields can sometimes cause the optimistic update to not apply when we
611accidentally cause any query that needs to update accordingly to only be partially cached. In other
612words, if our optimistic updates cause a cache miss, we won’t see them being applied.
613
614Sometimes we may need to apply optimistic updates to fields that accept arguments. For instance, our
615`favorite` field may have a date cut-off:
616
617```graphql
618mutation FavoriteTodo(id: $id) {
619 favoriteTodo(id: $id) {
620 id
621 favorite(since: ONE_MONTH_AGO)
622 updatedAt
623 }
624}
625```
626
627To solve this, we can return a method on the optimistic result our `optimistic` update function
628returns:
629
630```js
631const cache = cacheExchange({
632 optimistic: {
633 favoriteTodo(args, cache, info) {
634 return {
635 __typename: 'Todo',
636 id: args.id,
637 favorite(_args, cache, info) {
638 return true;
639 },
640 },
641 },
642 },
643});
644```
645
646The function signature and arguments it receives is identical to the toplevel optimistic function
647you define, and is basically like a nested optimistic function.
648
649### Variables for Optimistic Updates
650
651Sometimes it's not possible for us to retrieve all data that an optimistic update requires to create
652a "fake result" from the cache or from all existing variables.
653
654This is why Graphcache allows for a small escape hatch for these scenarios, which allows us to access
655additional variables, which we may want to pass from our UI code to the mutation. For instance, given
656a mutation like the following we may add more variables than the mutation specifies:
657
658```graphql
659mutation UpdateTodo($id: ID!, $text: ID!) {
660 updateTodo(id: $id, text: $text) {
661 id
662 text
663 }
664}
665```
666
667In the above mutation we've only defined an `$id` and `$text` variable. Graphcache typically filters
668variables using our query document definitions, which means that our API will never receive any
669variables other than the ones we've defined.
670
671However, we're able to pass additional variables to our mutation, e.g. `{ extra }`, and since
672`$extra` isn't defined it will be filtered once the mutation is sent to the API. An optimistic
673mutation however will still be able to access this variable, like so:
674
675```js
676cacheExchange({
677 updates: {
678 Mutation: {
679 updateTodo(_result, _args, _cache, info) {
680 const extraVariable = info.variables.extra;
681 },
682 },
683 },
684});
685```
686
687### Reading on
688
689[On the next page we'll learn about "Schema Awareness".](./schema-awareness.md)