Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

(docs) - Overhaul "Local Resolvers" & "Cache Updates" Graphcache docs pages (#1412)

* Rename "Computed Queries" to "Local Resolvers"

* Expand "Local Resolvers" introduction section

* Add section explaining record resolvers to "Local Resolvers"

* Add "Resolving Entities" section to "Local Resolvers"

* Add section on cache.resolve to "Local Resolvers"

* Restructure remaining content on "Local Resolvers"

* Update prior links from "Computed Queries" to "Local Resolvers"

* Update "Cache Updates" introduction

* Add new sections to "Cache Updates"

* Replace remaining content on "Cache Updates"

* Expand and update "Optimistic Update" section

* Update local link to "Local Resolvers" and "Cache Updates"

authored by

Phil Pluckthun and committed by
GitHub
434b8e9a d0baa7f2

+909 -462
+4 -4
docs/api/graphcache.md
··· 343 343 ); 344 344 ``` 345 345 346 - [Read more about using `readFragment` on the ["Computed Queries" 347 - page.](../graphcache/computed-queries.md#reading-a-fragment) 346 + [Read more about using `readFragment` on the ["Local Resolvers" 347 + page.](../graphcache/local-resolvers.md#reading-a-fragment) 348 348 349 349 ### readQuery 350 350 ··· 368 368 ); // Data or null 369 369 ``` 370 370 371 - [Read more about using `readQuery` on the ["Computed Queries" 372 - page.](../graphcache/computed-queries.md#reading-a-query) 371 + [Read more about using `readQuery` on the ["Local Resolvers" 372 + page.](../graphcache/local-resolvers.md#reading-a-query) 373 373 374 374 ### writeFragment 375 375
+1 -1
docs/graphcache/README.md
··· 28 28 a normalized data structure. Query, mutation, and subscription results may update one another if 29 29 they share data, and the app will rerender or refetch data accordingly. This often allows your app 30 30 to make fewer API requests, since data may already be in the cache. 31 - - 💾 [**Custom cache resolvers**](./computed-queries.md) Since all queries are fully resolved in the 31 + - 💾 [**Custom cache resolvers**](./local-resolvers.md) Since all queries are fully resolved in the 32 32 cache before and after they're sent, you can add custom resolvers that enable you to format data, 33 33 implement pagination, or implement cache redirects. 34 34 - 💭 [**Subscription and Mutation updates**](./custom-updates.md) You can implement update functions
+362 -184
docs/graphcache/cache-updates.md
··· 5 5 6 6 # Cache Updates 7 7 8 - Every time Graphcache sees a result from the API for a `subscription` or a `mutation` it will look at the response and traverse it. 9 - This process is the same as for queries but instead of starting at the Query root type, 10 - it will start searching for keyable entities, where an object's `__typename` and `id` or `_id` fields (or a custom keys config) 11 - are provided, so it can write normalized entities to the cache. 8 + As we've learned [on the page on "Normalized 9 + Caching"](../normalized-caching.md#normalizing-relational-data), when Graphcache receives an API 10 + result it will traverse and store all its data to its cache in a normalised structure. Each entity 11 + that is found in a result will be stored under the entity's key. 12 + 13 + A query's result is represented as a graph, which can also be understood as a tree structure, 14 + starting from the root `Query` entity which then connects to other entities via links, which are 15 + relations stored as keys, where each entity has records that store scalar values, which are the 16 + tree's leafs. On the previous page, on ["Local Resolvers"](./local-resolvers.md), we've seen how 17 + resolvers can be attached to fields to manually resolve other entities (or transform record fields). 18 + Local Resolvers passively _compute_ results and change how Graphcache traverses and sees its locally 19 + cached data, however, for **mutations** and **subscriptions** we cannot passively compute data. 20 + 21 + When Graphcache receives a mutation or subscription result it still traverses it using the query 22 + document 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 queries 27 + > in our application. 12 28 13 - A normalized cache can't automatically assume that unrelated links have changed due to 14 - a mutation, since this is server-side specific logic. Instead, we may use the `updates` 15 - configuration to set up manual updates that react to mutations or subscriptions. 29 + This means that mutations and subscriptions still write and update entities in the cache. These 30 + updates are then reflected on all queries that our app currently uses. However, there are 31 + limitations to this. While resolvers can be used to passively change data for queries, for mutations 32 + and subscriptions we sometimes have to write **updaters** to update links and relations. 33 + This is often necessary when a given mutation or subscription deliver a result that is more granular 34 + than the cache needs to update all affected entities. 16 35 17 - ## Data Updates 36 + Previously, we've learned about cache updates [on the "Normalized Caching" 37 + page](normalized-caching/#manual-cache-updates). 18 38 19 - The `updates` configuration is similar to our `resolvers` configuration that we've [previously looked 20 - at on the "Computed Queries" page.](./computed-queries.md) We pass a resolver-like function into the 21 - configuration that receives cache-specific arguments. Instead of the `parent` argument we'll however 22 - receive the subscription's or mutation's `data` instead. 39 + The `updates` option on `cacheExchange` accepts a map for `Mutation` or `Subscription` keys on which 40 + we can add "updater functions" to react to mutation or subscription results. These `updates` 41 + functions look similar to ["Local Resolvers"](./local-resolvers.md) that we've seen in the last 42 + section and similar to [GraphQL.js' resolvers on the 43 + server-side](https://www.graphql-tools.com/docs/resolvers/). 23 44 24 45 ```js 25 - const cache = cacheExchange({ 46 + cacheExchange({ 26 47 updates: { 27 48 Mutation: { 28 - addTodo: (result, args, cache, info) => { 49 + mutationField: (result, args, cache, info) => { 29 50 // ... 30 51 }, 31 52 }, 32 53 Subscription: { 33 - newTodo: (result, args, cache, info) => { 54 + subscriptionField: (result, args, cache, info) => { 34 55 // ... 35 56 }, 36 57 }, 37 58 }, 38 - }); 59 + }) 60 + ``` 61 + 62 + An "updater" may be attached to a `Mutation` or `Subscription` field and accepts four positional 63 + arguments, which are the same as [the resolvers' arguments](./local-resolvers.md): 64 + 65 + - `result`: The full API result that's currently being written to the cache. Typically we'd want to 66 + avoid coupling by only looking at the current field that the updater is attached to, but it's 67 + worth noting that we can access any part of the result. 68 + - `args`: The arguments that the field has been called with, which will be replaced with an empty 69 + object if the field hasn't been called with any arguments. 70 + - `cache`: The `cache` instance, which gives us access to methods allowing us to interact with the 71 + - local cache. Its full API can be found [in the API docs](../api/graphcache.md#cache). On this page 72 + we use it frequently to read from and write to the cache. 73 + - `info`: This argument shouldn't be used frequently but it contains running information about the 74 + traversal of the query document. It allows us to make resolvers reusable or to retrieve 75 + information about the entire query. Its full API can be found [in the API 76 + docs](../api/graphcache.md#info). 77 + 78 + The cache updaters return value is disregarded (and typed as `void` in TypeScript), which makes any 79 + method that they call on the `cache` instance a side-effect, which may trigger additional cache 80 + changes and updates all affected queries as we modify them. 81 + 82 + ## Manually updating entities 83 + 84 + If a mutation field's result isn't returning the full entity it updates then it becomes impossible 85 + for Graphcache to update said entity automatically. For instance, we may have a mutation like the 86 + following: 87 + 88 + ```graphql 89 + mutation UpdateTodo ($todoId: ID!, $date: String!) { 90 + updateTodoDate(id: $todoId, date: $date) 91 + } 39 92 ``` 40 93 41 - Inside these update functions, apart from the `cache` methods that we've seen in ["Computed 42 - Query"](./computed-queries.md) to read from the cached data, we can also use other `cache` methods to 43 - write to the cached data. 94 + In this hypothetical case instead of `Mutation.updateDate` resolving to the full `Todo` object type 95 + it instead results in a scalar. This could be fixed by changing the `Mutation` in our API's schema 96 + to instead return the full `Todo` entity, which would allow us to run the mutation as such, which 97 + updates the `Todo` in our cache automatically: 44 98 45 - ### cache.updateQuery 99 + ```graphql 100 + mutation UpdateTodo($todoId: ID!, $date: String!) { 101 + updateTodoDate(id: $todoId, date: $date) { 102 + ...Todo_date 103 + } 104 + } 46 105 47 - The first we'll look at is `cache.updateQuery`. This method accepts a `{ query, variables }` object 48 - as the first argument and an updater callback as the second. The updater function receives the query 49 - data as its' only argument and must return the updated version of said data. 106 + fragment Todo_date on Todo { 107 + id 108 + updatedAt 109 + } 110 + ``` 50 111 51 - Note that we don't have to update the query data immutably. _Graphcache_ never returns raw data and 52 - will instead always return copies of data. This means that we can also mutate query data inside the 53 - `updateQuery` callback. 112 + However, if this isn't possible we can instead write an updater that updates our `Todo` entity 113 + manually by using the `cache.writeFragment` method: 54 114 55 115 ```js 56 - const TodosQuery = gql` 57 - query { 58 - __typename 59 - todos { 60 - __typename 61 - id 62 - text 63 - complete 64 - } 65 - } 66 - `; 116 + import { gql } from '@urql/core'; 67 117 68 - const cache = cacheExchange({ 118 + cacheExchange({ 69 119 updates: { 70 120 Mutation: { 71 - addTodo: (result, args, cache, info) => { 72 - cache.updateQuery({ query: TodosQuery }, data => { 73 - data.todos.push(result.addTodo); 74 - return data; 75 - }); 121 + updateTodoDate(_result, args, cache, _info) { 122 + const fragment = gql` 123 + fragment _ on Todo { 124 + id 125 + updatedAt 126 + } 127 + `; 128 + 129 + cache.writeFragment( 130 + fragment, 131 + { id: args.id, updatedAt: args.date }, 132 + ); 76 133 }, 77 134 }, 78 135 }, 79 136 }); 80 137 ``` 81 138 82 - In the above example, we add an updater configuration for `Mutation.addTodo`. Whenever a mutation's 83 - result contains `addTodo` our new updater will be called. 139 + The `cache.writeFragment` method is similar to the `cache.readFragment` method that we've seen [on 140 + the "Local Resolvers" page before](./local-resolvers.md#reading-a-fragment). Instead of reading data 141 + for a given fragment it instead writes data to the cache. 84 142 85 - Inside the updater we use `cache.updateQuery` to update a list of todos with the new todo that has 86 - been created by `addTodo`, which we can access using `result.addTodo`. 87 - We could also alter this todo before returning our updated `data`. 143 + > **Note:** In the above example, we've used 144 + > [the `gql` tag function](../api/core.md#gql) because `writeFragment` only accepts 145 + > GraphQL `DocumentNode`s as inputs, and not strings. 88 146 89 - With this configuration any query that requests `Query.todos` will automatically update and contain 90 - our new todo, when a mutation with `Mutation.addTodo` completes. 147 + ### Cache Updates outside of updates 91 148 92 - It's worth noting that it doesn't matter whether the `TodosQuery` is the same one that you use in 93 - your application code. We're simply updating the normalized data of `Query.todos` across the 94 - normalized store, which will be reflected in any query that depends on `Query.todos`. 149 + Cache updates are **not** possible outside of `updates`. If we attempt to store the `cache` in a 150 + variable and call its methods outside of any `updates` functions (or functions, like `resolvers`) 151 + then Graphcache will throw an error. 95 152 96 - ### cache.writeFragment 153 + Methods like these cannot be called outside of the `cacheExchange`'s `updates` functions, because 154 + all updates are isolated to be _reactive_ to mutations and subscription events. In Graphcache, 155 + out-of-band updates aren't permitted because the cache attempts to only represent the server's 156 + state. This limitation keeps the data of the cache true to the server data we receive from API 157 + results and makes its behaviour much more predictable. 97 158 98 - Similar to `cache.updateQuery`, we can also update data for a fragment using `cache.writeFragment`, 99 - instead of an entire query. This method accepts a GraphQL fragment instead of an entire GraphQL 100 - query. It's also not an updater method but a direct write method instead. 159 + If we still manage to call any of the cache's methods outside of its callbacks in its configuration, 160 + we will receive [a "(2) Invalid Cache Call" error](./errors.md#2-invalid-cache-call). 101 161 102 - The first argument for `cache.writeFragment`, similarly to `readFragment`, must be a GraphQL 103 - fragment. The second argument is the data that should be written to the cache. This data object must 104 - contain `id` or another field if the type has a custom `keys` configuration. 162 + ## Updating lists or links 105 163 106 - ```js 107 - import { gql } from '@urql/core'; 164 + Mutations that create new entities are pretty common, and it's not uncommon to attempt to update the 165 + cache when a mutation result for these "creation" mutations come back, since this avoids an 166 + additional roundtrip to our APIs. 108 167 109 - cache.writeFragment( 110 - gql` 111 - fragment _ on Todo { 112 - id 113 - text 114 - } 115 - `, 116 - { 117 - id: '1', 118 - text: 'update', 168 + While it's possible for these mutations to return any affected entities that carry the lists as 169 + well, often these lists live on fields on or below the `Query` root type, which means that we'd be 170 + sending a rather large API result. For large amounts of pages this is especially infeasible. 171 + Instead, most schemas opt to instead just return the entity that's just been created: 172 + 173 + ```graphql 174 + mutation NewTodo ($text: String!) { 175 + createTodo(id: $todoId, text: $text) { 176 + id 177 + text 119 178 } 120 - ); 179 + } 121 180 ``` 122 181 123 - > **Note:** In the above example, we've used 124 - > [the `gql` tag function](../api/core.md#gql) because `writeFragment` only accepts 125 - > GraphQL `DocumentNode`s as inputs, and not strings. 126 - 127 - This can be useful for instance if we have a mutation that doesn't return the type that the GraphQL 128 - API will alter in the background. Suppose we had a `updateTodoText` mutation that doesn't allow us 129 - to access the updated `Todo`. In such a case `cache.writeFragment` allows us to make the change 130 - manually: 182 + If we have a corresponding field on `Query.todos` that contains all of our `Todo` entities then this 183 + means that we'll need to create an updater that automatically adds the `Todo` to our list: 131 184 132 185 ```js 133 - import { gql } from '@urql/core'; 134 - 135 - const cache = cacheExchange({ 186 + cacheExchange({ 136 187 updates: { 137 188 Mutation: { 138 - updateTodoText: (result, args, cache, info) => { 139 - cache.writeFragment( 140 - gql` 141 - fragment _ on Todo { 142 - id 143 - text 144 - } 145 - `, 146 - { id: args.id, text: args.text } 147 - ); 189 + updateTodoDate(result, _args, cache, _info) { 190 + const TodoList = gql`{ todos { id } }`; 191 + 192 + cache.updateQuery({ query: TodoList }, data => { 193 + data.todos.push(result.createTodo); 194 + return data; 195 + }); 148 196 }, 149 197 }, 150 198 }, 151 199 }); 152 200 ``` 153 201 154 - In this example we haven't used `result` at all, but have written to a `Todo` fragment using the 155 - arguments (`args`) that have been supplied to `Mutation.updateTodoText`. This can also be used in 156 - combination with `cache.readFragment` or `cache.resolve` if we need to retrieve arbitrary data from 157 - the cache, before using `cache.writeFragment` to update some data. 202 + Here we use the `cache.updateQuery` method, which is similar to the `cache.readQuery` method that 203 + we've seen on the "Local Resolvers" page before](./local-resolvers.md#reading-a-query). 158 204 159 - ## cache.invalidate 205 + This method accepts a callback which will give us the `data` of the query, as read from the locally 206 + cached data and we may return an updated version of this data. While we may want to instinctively 207 + opt for immutably copying and modifying this data, we're actually allowed to mutate it directly, 208 + since it's just a copy of the data that's been read by the cache. 160 209 161 - The `cache.invalidate` method is useful for evicting a single entity from the cache. When a user 162 - logs out or a mutation deletes an item from a list it can be tedious to update every link in our 163 - normalized data to said entity, instead the `cache.invalidate` method can be used to quickly remove 164 - the entity itself. 210 + This `data` may also be `null` if the cache doesn't actually have enough locally cached information 211 + to fulfil the query. This is important because resolvers aren't actually applied to cache methods in 212 + updaters. All resolvers are ignored so it becomes impossible to accidentally commit transformed data 213 + to our cache. We could safely add a resolver for `Todo.createdAt` and wouldn't have to worry about 214 + an updater accidentally writing it to the cache's internal data structure. 165 215 166 - Note that this may lead to an additional request to the GraphQL API, unless you're making use of the 167 - ["Schema Awareness" feature](./schema-awareness.md), since deleting an entity may cause cache 168 - misses for all queries that depend on this entity. 216 + ## Updating many unknown links 217 + 218 + In the previous section we've seen how to update data, like a list, when a mutation result enters 219 + the cache. However, we've used a rather simple example when we've looked at a single list on a known 220 + field. 221 + 222 + In many schemas pagination is quite common and when we for instance delete a todo then knowing which 223 + list to update becomes unknowable. We cannot know ahead of time how many pages (and using which 224 + variables) we've already accessed. This knowledge in fact _shouldn't_ be available to Graphcache. 225 + Querying the `Client` is an entirely separate concern that's often colocated with some part of our 226 + UI code. 227 + 228 + ```graphql 229 + mutation RemoveTodo ($id: ID!) { 230 + removeTodo(id: $id) 231 + } 232 + ``` 233 + 234 + Suppose we have the above mutation which deletes a `Todo` entity by its ID. Our app may query a list 235 + of these items over many pages with separate queries being sent to our API, which makes it hard to 236 + know which fields should be checked: 237 + 238 + ```graphql 239 + query PaginatedTodos ($skip: Int) { 240 + todos(skip: $skip) { 241 + id 242 + text 243 + } 244 + } 245 + ``` 246 + 247 + Instead, we can **introspect an entity's fields** to find out dynamically which fields we may want 248 + to update. This is possible thanks to [the `cache.inspectFields` 249 + method](../api/graphcache.md#inspectfields). This method accepts a key or a keyable entity like the 250 + `cache.keyOfEntity` method that [we've seen on the "Local Resolvers" 251 + page](./local-resolvers.md#resolving-by-keys) or the `cache.resolve` method's first argument. 169 252 170 253 ```js 171 - const cache = cacheExchange({ 254 + cacheExchange({ 172 255 updates: { 173 256 Mutation: { 174 - deleteTodo: (result, args, cache, info) => { 175 - cache.invalidate({ __typename: 'Todo', id: args.id }); 257 + removeTodo(_result, args, cache, _info) { 258 + const TodoList = gql` 259 + query (skip: $skip) { 260 + todos(skip: $skip) { id } 261 + } 262 + `; 263 + 264 + const fields = cache.inspectFields('Query') 265 + .filter(field => field.fieldName === 'todos') 266 + .forEach(field => { 267 + cache.updateQuery( 268 + { 269 + query: TodoList, 270 + variables: { skip: field.arguments.skip }, 271 + }, 272 + data => { 273 + data.todos = data.todos.filter(todo => todo.id !== args.id); 274 + return data; 275 + } 276 + }); 277 + }); 176 278 }, 177 279 }, 178 280 }, 179 281 }); 180 282 ``` 181 283 182 - The above example deletes a `Todo` with a given `id` from the arguments, when `Mutation.deleteTodo` 183 - is executed. This will cause all queries that reference this `Todo` to automatically update. 284 + To implement an updater for our example's `removeTodo` mutation field we may use the 285 + `cache.inspectFields('Query')` method to retrieve a list of all fields on the `Query` root entity. 286 + This list will contain all known fields on the `"Query"` entity. Each field is described as an 287 + object with three properties: 288 + 289 + - `fieldName`: The field's name; in this case we're filtering for all `todos` listing fields. 290 + - `arguments`: The arguments for the given field, since each field that accepts arguments can be 291 + accessed multiple times with different arguments. In this example we're looking at 292 + `arguments.skip` to find all unique pages. 293 + - `fieldKey`: This is the field's key which can come in useful to retrieve a field using 294 + `cache.resolve(entityKey, fieldKey)` to prevent the arguments from having to be stringified 295 + repeatedly. 184 296 185 - ## cache.inspectFields 297 + To summarise, we filter the list of fields in our example down to only the `todos` fields and 298 + iterate over each of our `arguments` for the `todos` field to filter all lists to remove the `Todo` 299 + from them. 186 300 187 - It's possible that you may have to alter multiple parts of the normalized cache data all at once. 188 - For instance, you may want to see a field that has been called with different arguments, like a listing 189 - field. The `cache.inspectFields` method was made for this purpose and is able to return all fields 190 - that the cache has seen on a given type. 301 + ### Inspecting arbitary entities 302 + 303 + We're not required to only inspecting fields on the `Query` root entity. Instead, we can inspect 304 + fields on any entity by passing a different partial, keyable entity or key to `cache.inspectFields`. 191 305 192 - In this example we'll alter all fields on `Query.todos`: 306 + For instance, if we had a `Todo` entity and wanted to get all of its known fields then we could pass 307 + in a partial `Todo` entity just as well: 193 308 194 309 ```js 195 - const cache = cacheExchange({ 196 - updates: { 197 - Mutation: { 198 - addTodo: (result, args, cache, info) => { 199 - // Get all children of query, this can also be Todo if we would be looking for for instance the author subquery 200 - const allFields = cache.inspectFields("Query"); 201 - // Filter these children to the query you like, in our case query { todos } 202 - const todoQueries = allFields.filter(x => x.fieldName === "todos"); 203 - 204 - todosQueries.forEach(({ arguments }) => { 205 - cache.updateQuery( 206 - { query: TODOS_QUERY, variables: arguments }, 207 - data => { 208 - data.todos.push(result.addTodo); 209 - return data; 210 - }); 211 - ); 212 - }) 213 - }, 214 - }, 215 - }, 310 + cache.inspectFields({ 311 + __typename: 'Todo', 312 + id: args.id 216 313 }); 217 314 ``` 218 315 219 - Let's combine the above example with invalidating fields, imagine the scenario where we add a todo but 220 - rather than manually pushing it on all the lists we just want to refetch the lists. 316 + ## Invalidating Entities 317 + 318 + Admittedly, it's sometimes almost impossible to write updaters for all mutations. It's often even 319 + hard to predict what our APIs may do when they receive a mutation. An update of an entity may change 320 + the sorting of a list, or remove an item from a list in a way we can't predict, since we don't have 321 + access to a full database to run the API locally. 322 + 323 + In cases like these it may be advisable to trigger a refetch instead and let the cache update itself 324 + by sending queries that have invalidated data associated to them to our API again. This process is 325 + called **invalidation** since it removes data from Graphcache's locally cached data. 326 + 327 + We may use the cache's [`cache.invalidate` method](../api/graphcache.md#invalidate) to either 328 + invalidate entire entities or individual fields. It has the same signature as [the `cache.resolve` 329 + method](../api/graphcache.md#resolve) which we've already seen [on the "Local Resolvers" page as 330 + well](./local-resolvers.md#resolving-other-fields). We can simplify the previous update we've written 331 + with a call to `cache.invalidate`: 221 332 222 333 ```js 223 - const cache = cacheExchange({ 334 + cacheExchange({ 224 335 updates: { 225 336 Mutation: { 226 - addTodo: (result, args, cache, info) => { 227 - const todosQueries = cache.inspectFields('Query').filter(x => x.fieldName === 'todos'); 228 - 229 - todosQueries.forEach(({ fieldName, arguments: variables }) => { 230 - cache.invalidate('Query', fieldName, variables); 337 + removeTodo(_result, args, cache, _info) { 338 + cache.invalidate({ 339 + __typename: 'Todo', 340 + id: args.id, 231 341 }); 232 342 }, 233 343 }, ··· 235 345 }); 236 346 ``` 237 347 238 - Now when we come onto a list we'll know that this list needs to be refetched. 348 + Like any other cache update, this will cause all queries that use this `Todo` entity to be updated 349 + against the cache. Since we've invalidated the `Todo` item they're using these queries will be 350 + refetched and sent to our API. 239 351 240 - ### Inspecting a sub-field 352 + If we're using ["Schema Awareness"](./schema-awareness.md) then these queries' results may actually 353 + be temporarily updated with a partial result, but in general we should observe that queries with 354 + data that has been invalidated will be refetched as some of their data isn't cached anymore. 241 355 242 - We've seeen how to inspect fields for a root-field in the above, but what if your query looks like this: 356 + ### Invalidating individual fields 243 357 244 - ``` 245 - query { 246 - todo(id: "x") { 247 - id 248 - authors { 249 - id 250 - name 251 - } 252 - } 253 - } 254 - ``` 358 + We may also want to only invalidate individual fields, since maybe not all queries have to be 359 + immediately updated. We can pass a field (and optional arguments) to the `cache.invalidate` method 360 + as well to only invalidate a single field. 255 361 256 - Now we'd need to traverse all the `todos` to find which we need, but there's another solution. 257 - Rather than use `cache.inspectFields('Query')`, which would give us all queried `todo` fields with their arguments, we can instead provide an object as the argument to `inspectFields` asking for all `Todo` types for a given id. 362 + For instance, we can use this to invalidate our lists instead of invalidating the entity itself. 363 + This can be useful if we know that modifying an entity will cause our list to be sorted differently, 364 + for instance. 258 365 259 366 ```js 260 - cache.inspectFields({ __typename: 'Todo', id: args.id }); 367 + cacheExchange({ 368 + updates: { 369 + Mutation: { 370 + updateTodo(_result, args, cache, _info) { 371 + const key = 'Query'; 372 + const fields = cache.inspectFields(key) 373 + .filter(field => field.fieldName === 'todos') 374 + .forEach(field => { 375 + cache.invalidate(key, field.fieldKey); 376 + // or alternatively: 377 + cache.invalidate(key, field.fieldName, field.arguments); 378 + }); 379 + }, 380 + }, 381 + }, 382 + }); 261 383 ``` 262 384 263 - Now we'll get all fields for the given `todo` and can freely update the `authors`. 385 + In this example we've attached an updater to a `Mutation.updateTodo` field. We react to this 386 + mutation by enumerating all `todos` listing fields using `cache.inspectFields` and targetedly 387 + invalidate only these fields, which causes all queries using these listing fields to be refetched. 264 388 265 389 ## Optimistic updates 266 390 267 391 If we know what result a mutation may return, why wait for the GraphQL API to fulfill our mutations? 268 - The _Optimistic Updates_ configuration allows us to set up "temporary" results for mutations, which 269 - will be applied immediately. This is a great solution to reduce the waiting time for the user. 270 - 271 - > Note that an optimistic response is meant to be a temporary update to an entity until the server responds to your mutation. 272 - > This means that what you return here should reflect the shape of what the server will return. 273 392 393 + Additionally to the `updates` configuration we may also pass an `optimistic` option to the 394 + `cacheExchange` which is a factory function using which we can create a "virtual" result for a 395 + mutation. This temporary result can be applied immediately to the cache to give our users the 396 + illusion that mutations were executed immediately, which is a great method to reduce waiting time 397 + and to make our apps feel snappier. 274 398 This technique is often used with one-off mutations that are assumed to succeed, like starring a 275 399 repository, or liking a tweet. In such cases it's often desirable to make the interaction feel 276 400 as instant as possible. 277 401 278 - The `optimistic` configurations are similar to `resolvers` as well. We supply an function to 279 - `Mutation` fields that must return some data. When said mutation is then executed, _Graphcache_ 280 - applies a temporary update using the supplied optimistic data that is reverted when the real 281 - mutation completes and an actual result comes back from the API. The temporary update is also 282 - reverted if the GraphQL mutation fails. 402 + The `optimistic` configuration is similar to our `resolvers` or `updates` configuration, except that 403 + it only receives a single map for mutation fields. We can attach optimistic functions to any 404 + mutation field to make it generate an optimistic that is applied to the cache while the `Client` 405 + waits for a response from our API. An "optimistic" function accepts three positional arguments, 406 + which are the same as the resolvers' or updaters' arguments, except for the first one: 283 407 284 - The `optimistic` functions receive the same arguments as `resolvers` functions, except for `parent`: 408 + The `optimistic` functions receive the same arguments as `updates` functions, except for `parent`, 409 + since we don't have any server data to work with: 285 410 286 - - `variables` – The variables used to execute the mutation. 287 - - `cache` – The cache we've already seen in [resolvers](./computed-queries.md) and in the previous 288 - examples on this page. In optimistic updates it's useful to retrieve more data from the cache. 289 - - `info` – Contains the used fragments and field arguments. 411 + - `args`: The arguments that the field has been called with, which will be replaced with an empty 412 + object if the field hasn't been called with any arguments. 413 + - `cache`: The `cache` instance, which gives us access to methods allowing us to interact with the 414 + - local cache. Its full API can be found [in the API docs](../api/graphcache.md#cache). On this page 415 + we use it frequently to read from and write to the cache. 416 + - `info`: This argument shouldn't be used frequently but it contains running information about the 417 + traversal of the query document. It allows us to make resolvers reusable or to retrieve 418 + information about the entire query. Its full API can be found [in the API 419 + docs](../api/graphcache.md#info). 420 + 421 + The usual `parent` argument isn't present since optimistic functions don't have any server data to 422 + handle or deal with and instead create this data. When a mutation is run that contains one or more 423 + optimistic mutation fields, Graphcache picks these up and generates immediate changes which it 424 + applies to the cache. The `resolvers` functions also trigger as if the results were real server 425 + results. 426 + 427 + This modification is temporary. Once a result from the API comes back it's reverted, which leaves us 428 + in a state where the cache can apply the "real" result to the cache. 429 + 430 + > Note: While optimistic mutations are waiting for results from the API all queries that may alter 431 + > our optimistic data are paused (or rather queued up) and all optimistic mutations will be reverted 432 + > at the same time. This means that optimistic results can stack but will never accidentally be 433 + > confused with "real" data in your configuration. 290 434 291 435 In the following example we assume that we'd like to implement an optimistic result for a 292 - `favoriteTodo` mutation, which sets `favorite` on a `Todo` to `true`: 436 + `favoriteTodo` mutation. The mutation is rather simple and all we have to do is create a function 437 + that imitates the result that the API is assumed to send back: 293 438 294 439 ```js 295 440 const cache = cacheExchange({ ··· 303 448 }); 304 449 ``` 305 450 306 - Since we return an assumed result in our `optimistic` configuration, `Mutation.favoriteTodo` will be 307 - automatically applied and `favorite` will seemingly flip to `true` instantly for the queries that 308 - observe `Todo.query`. 451 + This optimistic mutation will be applied to the cache. If any `updates` configuration exists for 452 + `Mutation.favoriteTodo` then it will be executed using the optimistic result. 453 + Once the mutation result comes back from our API this temporary change will be rolled back and 454 + discarded. 455 + 456 + It's important to ensure that our optimistic mutations return all data that the real mutation may 457 + return. If our mutations request a field in their selection sets that our optimistic mutation 458 + doesn't contain then we'll see a warning, since this is a common mistake. To work around not having 459 + enough data we may use methods like `cache.readFragment` and `cache.resolve` to retrieve more data 460 + from our cache. 461 + 462 + ### Variables for Optimistic Updates 463 + 464 + Sometimes it's not possible for us to retrieve all data that an optimistic update requires to create 465 + a "fake result" from the cache or from all existing variables. 466 + 467 + This is why Graphcache allows for a small escape hatch for these scenarios which allows us to access 468 + additional variables which we may want to pass from our UI code to the mutation. For instance, given 469 + a mutation like the following we may add more variables than the mutation specifies: 470 + 471 + ```graphql 472 + mutation UpdateTodo ($id: ID!, $text: ID!) { 473 + updateTodo(id: $id, text: $text) { 474 + id 475 + text 476 + } 477 + } 478 + ``` 479 + 480 + In the above mutation we've only defined an `$id` and `$text` variable. Graphcache typically filters 481 + variables using our query document definitions, which means that our API will never receive any 482 + variables other than the ones we've defined. 483 + 484 + However, we're able to pass additional variables to our mutation, e.g. `{ extra }`, and since 485 + `$extra` isn't defined it will be filtered once the mutation is sent to the API. An optimistic 486 + mutation however will still be able to access this variable. 309 487 310 488 ### Reading on 311 489 312 - [On the next page we'll learn about "Schema-awareness".](./schema-awareness.md) 490 + [On the next page we'll learn about "Schema Awareness".](./schema-awareness.md)
-271
docs/graphcache/computed-queries.md
··· 1 - --- 2 - title: Computed Queries 3 - order: 2 4 - --- 5 - 6 - # Computed Queries 7 - 8 - When dealing with data we could have special cases where we want to transform 9 - the data between the API and frontend logic. For example: 10 - 11 - - alter the format of a date, perhaps from a UNIX timestamp to a `Date` object. 12 - - if we have a list of a certain entity in the cache and then want to query a 13 - specific entity, chances are this will already be (partially) available in the 14 - cache's list. 15 - 16 - These cases can be solved with the concept of `resolvers`. 17 - 18 - ## Resolvers 19 - 20 - Let's look at how we can introduce these `resolvers` to our Graphcache exchange. 21 - Let's say we have a `Todo` type with an `updatedAt` property which is a UNIX timestamp. 22 - 23 - ```js 24 - import { cacheExchange } from '@urql/exchange-graphcache'; 25 - 26 - const cache = cacheExchange({ 27 - resolvers: { 28 - Todo: { 29 - updatedAt(parent, args, cache, info) { 30 - return new Date(parent.updatedAt); 31 - }, 32 - }, 33 - }, 34 - }); 35 - ``` 36 - 37 - Now when we query our `todos` every time we encounter an object with `Todo` 38 - as the `__typename` it will convert the `parent.updatedAt` property to a `Date`. This way we 39 - can effectively change how we handle a certain property on an entity. 40 - 41 - Let's look at the arguments passed to `resolvers` to get a better sense of 42 - what we can do, there are four arguments (these are in order): 43 - 44 - - `parent` – The original entity in the cache. In the example above, this 45 - would be the full `Todo` object. 46 - - `args` – The arguments used in this field. 47 - - `cache` – This is the normalized cache. The cache provides us with `resolve`, `readQuery` and `readFragment` methods, 48 - read more about this [below](#resolve). 49 - - `info` – This contains the fragments used in the query and the field arguments in the query. 50 - 51 - ## Cache parameter 52 - 53 - This is the main point of communication with the cache, it will give us access to 54 - all cached data. 55 - 56 - ### resolve 57 - 58 - The `cache.resolve` method is used to get links and property values from the cache. 59 - Our cache methods have three arguments: 60 - 61 - - `entity` – This can either be an object containing a `__typename` and an `id` or 62 - `_id` field _or_ a string key leading to a cached entity. 63 - - `field` – The field we want data for. This can be a relation or a single property. 64 - - `arguments` – The arguments to include on the field. 65 - 66 - To get a better grasp let's look at a few examples, 67 - consider the following data structure: 68 - 69 - ```js 70 - todos: [ 71 - { 72 - id: '1', 73 - text: 'Install urql', 74 - complete: true, 75 - author: { id: '2', name: 'Bar' }, 76 - }, 77 - { 78 - id: '2', 79 - text: 'Learn urql', 80 - complete: true, 81 - author: { id: '1', name: 'Foo' }, 82 - }, 83 - ]; 84 - ``` 85 - 86 - Lets get the `author` for a todo. 87 - 88 - ```js 89 - const parent = { 90 - id: '1', 91 - text: 'Install urql', 92 - complete: true, 93 - author: undefined, 94 - __typename: 'Todo', 95 - }; 96 - 97 - console.log(cache.resolve(parent, 'author')); // 'Author:2' 98 - ``` 99 - 100 - Now we have a stringed key that identifies our author. We 101 - can use this to derive the name of our author. 102 - 103 - ```js 104 - const name = cache.resolve('Author:2', 'name'); 105 - console.log(name); // 'Bar' 106 - ``` 107 - 108 - This can help solve practical use cases like date formatting, 109 - where we would query the date and then convert it in our resolver. 110 - 111 - We can also link entities that come from a list, imagine the scenario where 112 - we have queried `todos` but now want the detailView of a single `todo`. 113 - 114 - ```js 115 - const cache = cacheExchange({ 116 - resolvers: { 117 - Query: { todo: (parent, args) => ({ __typename: 'Todo', id: args.id }) }, 118 - }, 119 - }); 120 - ``` 121 - 122 - Returning a `__typename` and `key` (`id`/`_id`/custom key) is sufficient to make the 123 - cache resolve this to the full entity. 124 - 125 - Note that resolving from a list to details can lead to partial data, this will result in 126 - a network-request to get the full data when fields are missing. 127 - When graphcache isn't [aware of our schema](./schema-awareness.md) it won't show partial data. 128 - 129 - ### Reading a query 130 - 131 - Another method the cache allows is to let us read a full query, this method 132 - accepts an object of `query` and optionally `variables`. 133 - 134 - ```js 135 - cache.readQuery({ query: Todos, variables: { from: 0, limit: 10 } })` 136 - ``` 137 - 138 - This way we'll get the stored data for the `TodosQuery` for the given `variables`. 139 - 140 - ### Reading a fragment 141 - 142 - The store allows the user to also read a fragment for a certain entity, this function 143 - accepts a `fragment` and an `id`. This looks like the following. 144 - 145 - ```js 146 - import { gql } from '@urql/core'; 147 - 148 - const data = cache.readFragment( 149 - gql` 150 - fragment _ on Todo { 151 - id 152 - text 153 - } 154 - `, 155 - '1' 156 - ); 157 - ``` 158 - 159 - > **Note:** In the above example, we've used 160 - > [the `gql` tag function](../api/core.md#gql) because `readFragment` only accepts 161 - > GraphQL `DocumentNode`s as inputs, and not strings. 162 - 163 - This way we'll get the Todo with id 1 and the relevant data we are asking for in the 164 - fragment. 165 - 166 - ## Pagination 167 - 168 - `Graphcache` offers some preset `resolvers` to help us out with endless scrolling pagination. 169 - 170 - ### Simple Pagination 171 - 172 - Given we have a schema that uses some form of `offset` and `limit` based pagination, we can use the 173 - `simplePagination` exported from `@urql/exchange-graphcache/extras` to achieve an endless scroller. 174 - 175 - This helper will concatenate all queries performed to one long data structure. 176 - 177 - ```js 178 - import { cacheExchange } from '@urql/exchange-graphcache'; 179 - import { simplePagination } from '@urql/exchange-graphcache/extras'; 180 - 181 - const cache = cacheExchange({ 182 - resolvers: { 183 - Query: { 184 - todos: simplePagination(), 185 - }, 186 - }, 187 - }); 188 - ``` 189 - 190 - This form of pagination accepts an object as an argument, we can specify two 191 - options in here `limitArgument` and `offsetArgument` these will default to `limit` 192 - and `skip` respectively. This way we can use the keywords that are in our queries. 193 - 194 - We may also add the `mergeMode` option, which defaults to `'after'` and can otherwise 195 - be set to `'before'`. This will handle in which order pages are merged when paginating. 196 - The default `after` mode assumes that pages that come in last should be merged 197 - _after_ the first pages. The `'before'` mode assumes that pages that come in last 198 - should be merged _before_ the first pages, which can be helpful in a reverse 199 - endless scroller (E.g. Chat App). 200 - 201 - Example series of requests: 202 - 203 - ``` 204 - // An example where mergeMode: after works better 205 - skip: 0, limit: 3 => 1, 2, 3 206 - skip: 3, limit: 3 => 4, 5, 6 207 - 208 - mergeMode: after => 1, 2, 3, 4, 5, 6 ✔️ 209 - mergeMode: before => 4, 5, 6, 1, 2, 3 210 - 211 - // An example where mergeMode: before works better 212 - skip: 0, limit: 3 => 4, 5, 6 213 - skip: 3, limit: 3 => 1, 2, 3 214 - 215 - mergeMode: after => 4, 5, 6, 1, 2, 3 216 - mergeMode: before => 1, 2, 3, 4, 5, 6 ✔️ 217 - ``` 218 - 219 - ### Relay Pagination 220 - 221 - Given we have a [relay-compatible schema](https://facebook.github.io/relay/graphql/connections.htm) 222 - on our backend, we can offer the possibility of endless data resolving. 223 - This means that when we fetch the next page in our data 224 - received in `useQuery` we'll see the previous pages as well. This is useful for 225 - endless scrolling. 226 - 227 - We can achieve this by importing `relayPagination` from `@urql/exchange-graphcache/extras`. 228 - 229 - ```js 230 - import { cacheExchange } from '@urql/exchange-graphcache'; 231 - import { relayPagination } from '@urql/exchange-graphcache/extras'; 232 - 233 - const cache = cacheExchange({ 234 - resolvers: { 235 - Query: { 236 - todos: relayPagination(), 237 - }, 238 - }, 239 - }); 240 - ``` 241 - 242 - `relayPagination` accepts an object of options, for now we are offering one 243 - option and that is the `mergeMode`. This defaults to `inwards` and can otherwise 244 - be set to `outwards`. This will handle how pages are merged when we paginate 245 - forwards and backwards at the same time. outwards pagination assumes that pages 246 - that come in last should be merged before the first pages, so that the list 247 - grows outwards in both directions. The default inwards pagination assumes that 248 - pagination last pages is part of the same list and come after first pages. 249 - Hence it merges pages so that they converge in the middle. 250 - 251 - Example series of requests: 252 - 253 - ``` 254 - first: 1 => node 1, endCursor: a 255 - first: 1, after: 1 => node 2, endCursor: b 256 - ... 257 - last: 1 => node 99, startCursor: c 258 - last: 1, before: c => node 89, startCursor: d 259 - ``` 260 - 261 - With inwards merging the nodes will be in this order: `[1, 2, ..., 89, 99]` 262 - And with outwards merging: `[..., 89, 99, 1, 2, ...]` 263 - 264 - The helper happily supports schema that return nodes rather than 265 - individually-cursored edges. For each paginated type, we must either 266 - always request nodes, or always request edges -- otherwise the lists 267 - cannot be stiched together. 268 - 269 - ### Reading on 270 - 271 - [On the next page we'll learn about "Custom updates".](./custom-updates.md)
+536
docs/graphcache/local-resolvers.md
··· 1 + --- 2 + title: Local Resolvers 3 + order: 2 4 + --- 5 + 6 + # Local Resolvers 7 + 8 + Previously, we've learned about local resolvers [on the "Normalized Caching" 9 + page](./normalized-caching.md#manually-resolving-entities). They allow us to change the data that 10 + Graphcache reads as it queries against its local cache, return links that would otherwise not be 11 + cached, or even transform scalar records on the fly. 12 + 13 + The `resolvers` option on `cacheExchange` accepts a map of types with a nested map of fields, which 14 + means that we can add local resolvers to any field of any type. For example: 15 + 16 + ```js 17 + cacheExchange({ 18 + resolvers: { 19 + Todo: { 20 + updatedAt: parent => new Date(parent.updatedAt), 21 + }, 22 + }, 23 + }); 24 + ``` 25 + 26 + In the above example, what Graphcache does when it encounters the `updatedAt` field on `Todo` types. 27 + Similarly to how Graphcache knows [how to generate 28 + keys](./normalized-caching.md#custom-keys-and-non-keyable-entities) and looks up our custom `keys` 29 + configuration functions per `__typename`, it also uses our `resolvers` configuration on each field 30 + it queries from its locally cached data. 31 + 32 + A local resolver function in Graphcache has a similar signature to [GraphQL.js' resolvers on the 33 + server-side](https://www.graphql-tools.com/docs/resolvers/), so their shape should look familiar to 34 + us. 35 + 36 + ```js 37 + { 38 + TypeName: { 39 + fieldName: (parent, args, cache, info) => { 40 + return null; // new value 41 + }, 42 + }, 43 + } 44 + ``` 45 + 46 + A resolver may be attached to any type's field and accepts four positional arguments: 47 + 48 + - `parent`: The object on which the field will be added to, which contains the data as it's being 49 + queried. It will contain the current field's raw value if it's a scalar, which allows us to 50 + manipulate scalar values, like `parent.updatedAt` in the previous example. 51 + - `args`: The arguments that the field is being called with, which will be replaced with an empty 52 + object if the field hasn't been called with any arguments. For example, if the field is queried as 53 + `name(capitalize: true)` then `args` would be `{ capitalize: true }`. 54 + - `cache`: Unlike in GraphQL.js this will not be the context but a `cache` instance, which gives us 55 + access to methods allowing us to interact with the local cache. Its full API can be found [in the 56 + API docs](../api/graphcache.md#cache). 57 + - `info`: This argument shouldn't be used frequently but it contains running information about the 58 + traversal of the query document. It allows us to make resolvers reusable or to retrieve 59 + information about the entire query. Its full API can be found [in the API 60 + docs](../api/graphcache.md#info). 61 + 62 + The local resolvers may return any value that fits the query document's shape, however we must 63 + ensure that what we return matches the types of our schema. It for instance isn't possible to turn a 64 + record field into a link, i.e. replace a scalar with an entity. Instead, local resolvers are useful 65 + to transform records, like dates in our previous example, or to imitate server-side logic to allow 66 + Graphcache to retrieve more data from its cache without sending a query to our API. 67 + 68 + ## Transforming Records 69 + 70 + As we've explored in the ["Normalized Caching" page's section on 71 + records](./normalized-caching.md#storing-normalized-data), "records" are scalars and any fields in 72 + your query without selection sets. This could be a field with a string value, number, or any other 73 + field that resolves to a [scalar type](https://graphql.org/learn/schema/#scalar-types) rather than 74 + another entity i.e. object type. 75 + 76 + At the beginning of this page we've already seen an example of a local resolver that we've attached 77 + to a record field where we've added a resolver to a `Todo.updatedAt` field: 78 + 79 + ```js 80 + cacheExchange({ 81 + resolvers: { 82 + Todo: { 83 + updatedAt: parent => new Date(parent.updatedAt), 84 + }, 85 + }, 86 + }); 87 + ``` 88 + 89 + A query that contains this field may look like `{ todo { updatedAt } }`, which clearly shows us that 90 + this field is a scalar since it doesn't have any selection set on the `updatedAt` field. In our 91 + example, we access this field's value and parse it as a `new Date()`. 92 + 93 + This shows us that it doesn't matter for scalar fields what kind of value we return. We may parse 94 + strings into more granular JS-native objects or replace values entirely. 95 + 96 + We may also run into situations where we'd like to generalise the resolver and not make it dependent 97 + on the exact field it's being attached to. In these cases, the [`info` 98 + object](../api/graphcache.md#info) can be very helpful as it provides us information about the 99 + current query traversal and which part of the query document the cache is currently processing. The 100 + `info.fieldName` property is one of these properties and lets us know which field the resolver is 101 + currently operating on. Hence, we can create a reusable resolver like so: 102 + 103 + ```js 104 + const transformToDate = (parent, _args, _cache, info) => 105 + new Date(parent[info.fieldName]); 106 + 107 + cacheExchange({ 108 + resolvers: { 109 + Todo: { updatedAt: transformToDate }, 110 + }, 111 + }); 112 + ``` 113 + 114 + The resolver is now much more reusable, which is particularly handy if we're creating resolvers that 115 + we'd like to apply to multiple fields. The [`info` object has several more 116 + fields](../api/graphcache.md#info) that are all similarly useful to abstract our resolvers. 117 + 118 + We also haven't seen yet how to handle a field's arguments. 119 + If we have a field that accepts arguments we can use those as well as they're passed to us with the 120 + second argument of a resolver: 121 + 122 + ```js 123 + cacheExchange({ 124 + resolvers: { 125 + Todo: { 126 + text: (parent, args) => { 127 + return args.capitalize && parent.text 128 + ? parent.text.toUpperCase() 129 + : parent.text; 130 + }, 131 + }, 132 + }, 133 + }); 134 + ``` 135 + 136 + This is actually unlikely to be of use with records and scalar values as our API will have to be 137 + able to use these arguments just as well. In other words, while you may be able to pass any 138 + arguments to a field in your query, your GraphQL API's schema must accept these arguments in the 139 + first place. However, this is still useful if we're trying to imitate what the API is doing, which 140 + will become more relevant in the following examples and sections. 141 + 142 + ## Resolving Entities 143 + 144 + We've already briefly seen that resolvers can be used to replace a link in Graphcache's local data 145 + on the ["Normalized Caching" page](./normalized-caching.md#manually-resolving-entities). 146 + 147 + Given that Graphcache [stores entities in a normalized data 148 + structure](./normalized-caching.md#storing-normalized-data) there may be multiple fields on a given 149 + schema that can be used to get to the same entity. For instance, the schema may allow for the same 150 + entity to be looked up by an ID while this entity may also appear somewhere else in a list or on an 151 + entirely different field. 152 + 153 + When links (or relations) like these are cached by Graphcache it is able to look up the entities 154 + automatically, e.g. if we've sent a `{ todo(id: 1) { id } }` query to our API once then Graphcache 155 + will have seen that this field leads to the entity it returns and can query it automatically from 156 + its cache. 157 + 158 + However, if we have a list like `{ todos { id } }` we may have seen and cached a specific entity, 159 + but as we browse the app and query for `{ todo(id: 1) { id } }`, Graphcache isn't able to 160 + automatically find this entity even if it has cached it already and will send a request to our API. 161 + 162 + In many cases we can create a local resolvers to instead tell the cache where to look for a specific 163 + entity by returning partial information for it. Any resolver on a relational field, meaning any 164 + field that links to an object type (or a list of object types) in the schema, may return a partial 165 + entity that tells the cache how to resolve it. Hence, we're able to implement a resolver for the 166 + previously shown `todo(id: $id)` field as such: 167 + 168 + ```js 169 + cacheExchange({ 170 + resolvers: { 171 + Query: { 172 + todo: (_, args) => ({ __typename: 'Todo', id: args.id }), 173 + }, 174 + }, 175 + }); 176 + ``` 177 + 178 + The `__typename` field is required. Graphcache will [use its keying 179 + logic](./normalized-caching.md#custom-keys-and-non-keyable-entities) and your custom `keys` 180 + configuration to generate a key for this entity and will then be able to look this entity up in its 181 + local cache. As with regular queries, the resolver is known to return a link since the `todo(id: 182 + $id) { id }` will be used with a selection set, querying fields on the entity. 183 + 184 + ### Resolving by keys 185 + 186 + Resolvers can also directly return keys. We've previously learned [on the "Normalized Caching" 187 + page](./normalized-caching.md#custom-keys-and-non-keyable-entities) that the key for our example above 188 + would look something like `"Todo:1"` for `todo(id: 1)`. While it isn't adivsable to create keys 189 + manually in your resolvers, if you returned a key directly this would still work. 190 + 191 + Essentially, returning `{ __typename, id }` may sometimes be the same as returning the key manually. 192 + The `cache` that we receive as an argument on resolvers has a method for this logic, [the 193 + `cache.keyOfEntity` method](../api/graphcache.md#keyofentity). 194 + 195 + While it doesn't make much sense in this case, our example can be rewritten as: 196 + 197 + ```js 198 + cacheExchange({ 199 + resolvers: { 200 + Query: { 201 + todo: (_, args, cache) => 202 + cache.keyOfEntity({ __typename: 'Todo', id: args.id }), 203 + }, 204 + }, 205 + }); 206 + ``` 207 + 208 + And while it's not advisable to create keys ourselves, the resolvers' `cache` and `info` arguments 209 + give us ample opportunities to use and pass around keys. 210 + 211 + One example is the `info.parentKey` property. This property [on the `info` 212 + object](../api/graphcache.md#info) will always be set to the key of the entity that the resolver is 213 + currently run on. For instance, for the above resolver it may be `"Query"`, for for a resolver on 214 + `Todo.updatedAt` it may be `"Todo:1"`. 215 + 216 + ## Resolving other fields 217 + 218 + In the above two examples we've seen how a resolver can replace Graphcache's logic which usually 219 + reads links and records only from its locally cached data. We've seen how a field on a record can 220 + use `parent[fieldName]` to access its cached record value and transform it and how a resolver for a 221 + link can return a partial entity [or a key](#resolving-by-keys). 222 + 223 + However sometimes we'll need to resolve data from other fields in our resolvers. 224 + 225 + For records, if the other field is on the same `parent` entity, it may seem logical to access it on 226 + `parent[otherFieldName]` as well, however the `parent` object will only be sparsely populated with 227 + fields that the cache has already queried prior to reaching the resolver. 228 + 229 + In the previous example, where we've created a resolver for `Todo.updatedAt` and accessed 230 + `parent.updatedAt` to transform its value the `parent.updatedAt` field is essentially a shortcut 231 + that allows us to get to the record quickly. 232 + 233 + Instead we can use the [the `cache.resolve` method](../api/graphcache.md#resolve). This method 234 + allows us to access Graphcache's cached data directly. It is used to resolve records or links on any 235 + given entity and accepts three arguments: 236 + 237 + - `entity`: This is the entity on which we'd like to access a field. We may either pass a keyable, 238 + partial entity, e.g. `{ __typename: 'Todo', id: 1 }` or a key. It takes the same inputs as [the 239 + `cache.keyOfEntity` method](../api/graphcache.md#keyofentity) which we've seen earlier in the 240 + ["Resolving by keys" section](#resolving-by-keys). It also accepts `null` which causes it to 241 + return `null`, which is useful for chaining multiple `resolve` calls for deeply accessing a field. 242 + - `fieldName`: This is the field's name we'd like to access. If we're looking for the record on 243 + `Todo.updatedAt` we would pass `"updatedAt"` and would receive the record value for this field. If 244 + we pass a field that is a _link_ to another entity then we'd pass that field's name (e.g. 245 + `"author"` for `Todo.author`) and `cache.resolve` will return a key instead of a record value. 246 + - `fieldArgs`: Optionally, as the third argument we may pass the field's arguments, e.g. `{ id: 1 }` 247 + if we're trying to access `todo(id: 1)` for instance. 248 + 249 + This means that we can rewrite our original `Todo.updatedAt` example as follows, if we'd like to 250 + avoid using the `parent[fieldName]` shortcut: 251 + 252 + ```js 253 + cacheExchange({ 254 + resolvers: { 255 + Todo: { 256 + updatedAt: (parent, _args, cache) => 257 + new Date(cache.resolve(parent, "updatedAt")), 258 + }, 259 + }, 260 + }); 261 + ``` 262 + 263 + When we call `cache.resolve(parent, "updatedAt")`, the cache will look up the `"updatedAt"` field on 264 + the `parent` entity, i.e. on the current `Todo` entity. 265 + We've also previously learned that `parent` may not contain all fields that the entity may have and 266 + may hence be missing its keyable fields, like `id`, so why does this then work? 267 + It works because `cache.resolve(parent)` is a shortcut for `cache.resolve(info.parentKey)`. 268 + 269 + Like the `info.fieldName` property `info.parentKey` gives us information about the current state of 270 + Graphcache's query operation. In this case, `info.parentKey` tells us what the parent's key is. 271 + However, since `cache.resolve(parent)` is much more intuitive we can write that instead since this 272 + is a supported shortcut. 273 + 274 + From this follows that we may also use `cache.resolve` to access other fields. Let's suppose we'd 275 + want `updatedAt` to default to the entity's `createdAt` field when it's actually `null`. In such a 276 + case we could write a resolver like so: 277 + 278 + ```js 279 + cacheExchange({ 280 + resolvers: { 281 + Todo: { 282 + updatedAt: (parent, _args, cache) => 283 + parent.updatedAt || cache.resolve(parent, "createdAt") 284 + }, 285 + }, 286 + }); 287 + ``` 288 + 289 + As we can see, we're effortlessly able to access other records from the cache, provided these fields 290 + are actually cached. If they aren't `cache.resolve` will return `null` instead. 291 + 292 + Beyond records, we're also able to resolve links and hence jump to records from another entity. 293 + Let's suppose we have an `author { id, createdAt }` field on the `Todo` and would like 294 + `Todo.createdAt` to simply copy the author's `createdAt` field. We can chain `cache.resolve` calls 295 + to get to this value: 296 + 297 + ```js 298 + cacheExchange({ 299 + resolvers: { 300 + Todo: { 301 + createdAt: (parent, _args, cache) => 302 + cache.resolve( 303 + cache.resolve(parent, "author"), /* "Author:1" */ 304 + "createdAt" 305 + ) 306 + }, 307 + }, 308 + }); 309 + ``` 310 + 311 + The return value of `cache.resolve` changes depending on what data the cache has stored. While it 312 + may return records for fields without selection sets, in other cases it may give you the key of 313 + other entities ("links") instead. It can even give you arrays of keys or records when the field's 314 + value contains a list. 315 + 316 + It's a pretty flexible method that allows us to access arbitrary values from our cache, however, we 317 + have to be careful about what value will be resolved by it, since the cache can't know itself what 318 + type of value it may return. 319 + 320 + The last trick this method allows you to apply is to access arbitrary fields on the root `Query` 321 + type. If we call `cache.resolve("Query", ...)` then we're also able to access arbitrary fields 322 + starting from the root `Query` of the cached data. (If you're using [Schema 323 + Awareness](./schema-awareness.md) the name `"Query"` may vary for you depending on your schema.) 324 + We're not constrained to accessing fields on the `parent` of a resolver but can also attempt to 325 + break out and access fields on any other entity we know of. 326 + 327 + ## Resolving Partial Data 328 + 329 + Local resolvers also allow for more advanced use-cases when it comes to links and object types. 330 + Previously we've seen how a resolver is able to link up a given field to an entity, which causes 331 + this field to resolve an entity directly instead of it being cecked against any cached links: 332 + 333 + ```js 334 + cacheExchange({ 335 + resolvers: { 336 + Query: { 337 + todo: (_, args) => ({ __typename: 'Todo', id: args.id }), 338 + }, 339 + }, 340 + }); 341 + ``` 342 + 343 + In this example, while `__typename` and `id` are required to make this entity keyable, we're also 344 + able to add on more fields to this object to override values later on in our selection. 345 + 346 + For instance, we can write a resolver that links `Query.todo` directly to our `Todo` entity but also 347 + only updates the `createdAt` field directly in the same resolver, if it is indeed accessed via the 348 + `Query.todo` field: 349 + 350 + ```js 351 + cacheExchange({ 352 + resolvers: { 353 + Query: { 354 + todo: (_, args) => ({ 355 + __typename: 'Todo', 356 + id: args.id, 357 + createdAt: new Date().toString(), 358 + }), 359 + }, 360 + }, 361 + }); 362 + ``` 363 + 364 + Here we've replaced the `createdAt` value of the `Todo` when it's accessed via this manual resolver. 365 + If it was accessed someplace else, for instance via a `Query.todos` listing field, this override 366 + wouldn't apply. 367 + 368 + We can even apply overrides to nested fields, which helps us to create complex resolvers for other 369 + use cases like pagination. 370 + 371 + [Read more on the topic of "Pagination" in the section below.](#pagination) 372 + 373 + ## Computed Queries 374 + 375 + We've now seen how the `cache` has several powerful methods, like [the `cache.resolve` 376 + method](../api/graphcache.md#resolve), which allow us to access any data in the cache while writing 377 + resolvers for individual fields. 378 + 379 + Additionally the cache has more methods that allow us to access more data at a time, like 380 + `cache.readQuery` and `cache.readFragment`. 381 + 382 + ### Reading a query 383 + 384 + At any point, the `cache` allows us to read entirely separate queries in our resolvers which starts 385 + a separate virtual operation in our resolvers. When we call `cache.readQuery` with a query and 386 + variables we can execute an entirely new GraphQL query against our cached data: 387 + 388 + ```js 389 + cache.readQuery({ query: Todos, variables: { from: 0, limit: 10 } })` 390 + ``` 391 + 392 + This way we'll get the stored data for the `TodosQuery` for the given `variables`. 393 + 394 + [Read more about `cache.readQuery` in the Graphcache API docs.](../api/graphcache.md#readquery) 395 + 396 + ### Reading a fragment 397 + 398 + The store also allows us to read a fragment for any given entity. The `cache.readFragment` method 399 + accepts a `fragment` and an `id`. This looks like the following. 400 + 401 + ```js 402 + import { gql } from '@urql/core'; 403 + 404 + const data = cache.readFragment( 405 + gql` 406 + fragment _ on Todo { 407 + id 408 + text 409 + } 410 + `, 411 + { id: 1 } 412 + ); 413 + ``` 414 + 415 + > **Note:** In the above example, we've used 416 + > [the `gql` tag function](../api/core.md#gql) because `readFragment` only accepts 417 + > GraphQL `DocumentNode`s as inputs, and not strings. 418 + 419 + This way we'll read the entire fragment that we've passed for the `Todo` for the given key, in this 420 + case `{ id: 1 }`. 421 + 422 + [Read more about `cache.readFragment` in the Graphcache API docs.](../api/graphcache.md#readfragment) 423 + 424 + ## Pagination 425 + 426 + `Graphcache` offers some preset `resolvers` to help us out with endless scrolling pagination, also 427 + known as "infinite pagination". It comes with two more advanced but generalised resolvers that can 428 + be applied to two specific pagination use-cases. 429 + 430 + They're not meant to implement infinite pagination for _any app_, instead they're useful when we'd 431 + like to add infinite pagination to an app quickly to try it out or if we're unable to replace it 432 + with separate components per page in environments like React Native, where a `FlatList` would 433 + require a flat, infinite list of items. 434 + 435 + ### Simple Pagination 436 + 437 + Given we have a schema that uses some form of `offset` and `limit` based pagination, we can use the 438 + `simplePagination` exported from `@urql/exchange-graphcache/extras` to achieve an endless scroller. 439 + 440 + This helper will concatenate all queries performed to one long data structure. 441 + 442 + ```js 443 + import { cacheExchange } from '@urql/exchange-graphcache'; 444 + import { simplePagination } from '@urql/exchange-graphcache/extras'; 445 + 446 + const cache = cacheExchange({ 447 + resolvers: { 448 + Query: { 449 + todos: simplePagination(), 450 + }, 451 + }, 452 + }); 453 + ``` 454 + 455 + This form of pagination accepts an object as an argument, we can specify two 456 + options in here `limitArgument` and `offsetArgument` these will default to `limit` 457 + and `skip` respectively. This way we can use the keywords that are in our queries. 458 + 459 + We may also add the `mergeMode` option, which defaults to `'after'` and can otherwise 460 + be set to `'before'`. This will handle in which order pages are merged when paginating. 461 + The default `after` mode assumes that pages that come in last should be merged 462 + _after_ the first pages. The `'before'` mode assumes that pages that come in last 463 + should be merged _before_ the first pages, which can be helpful in a reverse 464 + endless scroller (E.g. Chat App). 465 + 466 + Example series of requests: 467 + 468 + ``` 469 + // An example where mergeMode: after works better 470 + skip: 0, limit: 3 => 1, 2, 3 471 + skip: 3, limit: 3 => 4, 5, 6 472 + 473 + mergeMode: after => 1, 2, 3, 4, 5, 6 ✔️ 474 + mergeMode: before => 4, 5, 6, 1, 2, 3 475 + 476 + // An example where mergeMode: before works better 477 + skip: 0, limit: 3 => 4, 5, 6 478 + skip: 3, limit: 3 => 1, 2, 3 479 + 480 + mergeMode: after => 4, 5, 6, 1, 2, 3 481 + mergeMode: before => 1, 2, 3, 4, 5, 6 ✔️ 482 + ``` 483 + 484 + ### Relay Pagination 485 + 486 + Given we have a [relay-compatible schema](https://facebook.github.io/relay/graphql/connections.htm) 487 + on our backend, we can offer the possibility of endless data resolving. 488 + This means that when we fetch the next page in our data 489 + received in `useQuery` we'll see the previous pages as well. This is useful for 490 + endless scrolling. 491 + 492 + We can achieve this by importing `relayPagination` from `@urql/exchange-graphcache/extras`. 493 + 494 + ```js 495 + import { cacheExchange } from '@urql/exchange-graphcache'; 496 + import { relayPagination } from '@urql/exchange-graphcache/extras'; 497 + 498 + const cache = cacheExchange({ 499 + resolvers: { 500 + Query: { 501 + todos: relayPagination(), 502 + }, 503 + }, 504 + }); 505 + ``` 506 + 507 + `relayPagination` accepts an object of options, for now we are offering one 508 + option and that is the `mergeMode`. This defaults to `inwards` and can otherwise 509 + be set to `outwards`. This will handle how pages are merged when we paginate 510 + forwards and backwards at the same time. outwards pagination assumes that pages 511 + that come in last should be merged before the first pages, so that the list 512 + grows outwards in both directions. The default inwards pagination assumes that 513 + pagination last pages is part of the same list and come after first pages. 514 + Hence it merges pages so that they converge in the middle. 515 + 516 + Example series of requests: 517 + 518 + ``` 519 + first: 1 => node 1, endCursor: a 520 + first: 1, after: 1 => node 2, endCursor: b 521 + ... 522 + last: 1 => node 99, startCursor: c 523 + last: 1, before: c => node 89, startCursor: d 524 + ``` 525 + 526 + With inwards merging the nodes will be in this order: `[1, 2, ..., 89, 99]` 527 + And with outwards merging: `[..., 89, 99, 1, 2, ...]` 528 + 529 + The helper happily supports schema that return nodes rather than 530 + individually-cursored edges. For each paginated type, we must either 531 + always request nodes, or always request edges -- otherwise the lists 532 + cannot be stiched together. 533 + 534 + ### Reading on 535 + 536 + [On the next page we'll learn about "Cache Updates".](./cache-updates.md)
+2 -2
docs/graphcache/normalized-caching.md
··· 372 372 relational cached data of _Graphcache_, which means that it can emulate infinite pagination and 373 373 other complex behaviour. 374 374 375 - [Read more about local resolvers ont the following page, "Custom Queries".](./custom-queries.md) 375 + [Read more about resolvers on the following page about "Local Resolvers".](./local-resolvers.md) 376 376 377 377 ### Manual cache updates 378 378 ··· 445 445 affect other parts of the cache (like `Query.todos` here) beyond the automatic updates that a 446 446 normalized cache is expected to perform. 447 447 448 - [Read more about creating custom updates on the "Custom Updates" page.](./custom-updates.md) 448 + [Read more about writing cache updates on the "Cache Updates" page.](./cache-updates.md)
+4
packages/site/static.config.js
··· 78 78 path: '/docs/graphcache/custom-updates', 79 79 redirect: '/docs/graphcache/cache-updates', 80 80 }, 81 + { 82 + path: '/docs/graphcache/computed-queries', 83 + redirect: '/docs/graphcache/local-resolvers', 84 + }, 81 85 ], 82 86 };