Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 689 lines 28 kB view raw view rendered
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)