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.

Add additionalTypes option to context for default cache invalidation (#601)

* make proposal for additional invalidation possibilities, this could help solve the empty array problem for document-based caching

* golf implementation

* add type and test for additionalTypenames

* add to core api docs

* add changeset

* make entries for additionalTypenames in basics

* Apply suggestions from code review

Co-Authored-By: Will Golledge <35961363+wgolledge@users.noreply.github.com>

* move the additionalTypenames docs to document-caching

Co-authored-by: Will Golledge <35961363+wgolledge@users.noreply.github.com>

authored by

Jovi De Croock
Will Golledge
and committed by
GitHub
a41acd24 1ae7988f

+96 -15
+5
.changeset/shaggy-fans-grab.md
··· 1 + --- 2 + '@urql/core': minor 3 + --- 4 + 5 + Add `additionalTypenames` to the `OperationContext`, this allows the `document-cache` to invalidate efficiently when the `__typename` is unknown at the initial fetch.
+11 -10
docs/api/core.md
··· 194 194 Some of these options are set when the `Client` is initialised, so in the following list of 195 195 properties you'll likely see some options that exist on the `Client` as well. 196 196 197 - | Prop | Type | Description | 198 - | --------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | 199 - | fetchOptions | `?RequestInit \| (() => RequestInit)` | An optional [request policy](/basics/querying-data#request-policy) that should be used specifying the cache strategy. | 200 - | fetch | `typeof fetch` | An alternative implementation of `fetch` that will be used by the `fetchExchange` instead of `window.fetch` | 201 - | requestPolicy | `RequestPolicy` | An optional [request policy](/basics/querying-data#request-policy) that should be used specifying the cache strategy. | 202 - | url | `string` | The GraphQL endpoint | 203 - | pollInterval | `?number` | Every `pollInterval` milliseconds the query will be refetched. | 204 - | meta | `?OperationDebugMeta` | Metadata that is only available in development for devtools. | 205 - | suspense | `?boolean` | Whether suspense is enabled. | 206 - | preferGetMethod | `?number` | Instructs the `fetchExchange` to use HTTP GET for queries. | 197 + | Prop | Type | Description | 198 + | ------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | 199 + | fetchOptions | `?RequestInit \| (() => RequestInit)` | An optional [request policy](/basics/querying-data#request-policy) that should be used specifying the cache strategy. | 200 + | fetch | `typeof fetch` | An alternative implementation of `fetch` that will be used by the `fetchExchange` instead of `window.fetch` | 201 + | requestPolicy | `RequestPolicy` | An optional [request policy](/basics/querying-data#request-policy) that should be used specifying the cache strategy. | 202 + | url | `string` | The GraphQL endpoint | 203 + | pollInterval | `?number` | Every `pollInterval` milliseconds the query will be refetched. | 204 + | meta | `?OperationDebugMeta` | Metadata that is only available in development for devtools. | 205 + | suspense | `?boolean` | Whether suspense is enabled. | 206 + | preferGetMethod | `?number` | Instructs the `fetchExchange` to use HTTP GET for queries. | 207 + | additionalTypenames | `?number` | Allows you to tell the operation that it depends on certain typenames (used in document-cache.) | 207 208 208 209 It also accepts additional, untyped parameters that can be used to send more 209 210 information to custom exchanges.
+38 -3
docs/basics/document-caching.md
··· 50 50 This cache has a small trade-off! If we request a list of data and the API returns an empty list, 51 51 the cache won't be able to see the `__typename` of said list and won't invalidate. 52 52 53 - Once you've encountered this problem you've likely hit the limits of the _Document Caching_ 54 - approach, and you may want to [switch to "Normalized Caching" 55 - instead.](../graphcache/normalized-caching.md) 53 + There are two ways to fix this issue, supplying `additionalTypenames` to the context of your query or [switch to "Normalized Caching" 54 + instead](../graphcache/normalized-caching.md). 55 + 56 + ### Adding typenames 57 + 58 + This will elaborate about the first fix for empty lists, the `additionalTypenames`. 59 + 60 + Example where this would occur: 61 + 62 + ```js 63 + const query = `query { todos { id name } }`; 64 + const result = { todos: [] }; 65 + ``` 66 + 67 + At this point we don't know what types are possible for this query, so a best practice when using 68 + the default cache is to add `additionalTypenames` for this query. 69 + 70 + ```js 71 + // Keep the reference stable. 72 + const context = useMemo(() => ({ additionalTypenames: ['Todo'] }), []); 73 + const [result] = useQuery({ query, context }); 74 + ``` 75 + 76 + Now the cache will know when to invalidate this query even when the list is empty. 77 + 78 + We also have the possibility to use this for `mutations`. 79 + There are moments where a mutation can cause a side-effect on your server side and it needs 80 + to invalidate an additional entity. 81 + 82 + ```js 83 + const [result, execute] = useMutation(`mutation($name: String!) { createUser(name: $name) }`); 84 + 85 + const onClick = () => { 86 + execute({ context: { additionalTypenames: ['Wallet'] } }); 87 + }; 88 + ``` 89 + 90 + Now our `mutation` knows that when it completes it has an additional type to invalidate.
+31
packages/core/src/exchanges/cache.test.ts
··· 197 197 198 198 expect(reexecuteOperation).toBeCalledTimes(1); 199 199 }); 200 + 201 + it('retriggers query operation with additionalTypenames', () => { 202 + const typename = 'ExampleType'; 203 + const resultCache = new Map([[123, queryResponse]]); 204 + const operationCache = { [`${typename}-2`]: new Set([123]) }; 205 + 206 + afterMutation( 207 + resultCache, 208 + operationCache, 209 + exchangeArgs.client 210 + )({ 211 + ...mutationResponse, 212 + operation: { 213 + ...mutationResponse.operation, 214 + context: { 215 + ...mutationResponse.operation.context, 216 + additionalTypenames: [`${typename}-2`], 217 + }, 218 + }, 219 + data: { 220 + todos: [ 221 + { 222 + id: 1, 223 + __typename: typename, 224 + }, 225 + ], 226 + }, 227 + }); 228 + 229 + expect(reexecuteOperation).toBeCalledTimes(1); 230 + }); 200 231 }); 201 232 202 233 describe('on subscription', () => {
+10 -2
packages/core/src/exchanges/cache.ts
··· 129 129 client: Client 130 130 ) => (response: OperationResult) => { 131 131 const pendingOperations = new Set<number>(); 132 + const { additionalTypenames } = response.operation.context; 132 133 133 - collectTypesFromResponse(response.data).forEach(typeName => { 134 + [ 135 + ...collectTypesFromResponse(response.data), 136 + ...(additionalTypenames || []), 137 + ].forEach(typeName => { 134 138 const operations = 135 139 operationCache[typeName] || (operationCache[typeName] = new Set()); 136 140 operations.forEach(key => { ··· 154 158 operationCache: OperationCache 155 159 ) => (response: OperationResult) => { 156 160 const { operation, data, error } = response; 161 + const { additionalTypenames } = operation.context; 157 162 158 163 if (data === undefined || data === null) { 159 164 return; ··· 161 166 162 167 resultCache.set(operation.key, { operation, data, error }); 163 168 164 - collectTypesFromResponse(response.data).forEach(typeName => { 169 + [ 170 + ...collectTypesFromResponse(response.data), 171 + ...(additionalTypenames || []), 172 + ].forEach(typeName => { 165 173 const operations = 166 174 operationCache[typeName] || (operationCache[typeName] = new Set()); 167 175 operations.add(operation.key);
+1
packages/core/src/types.ts
··· 44 44 /** Additional metadata passed to [exchange]{@link Exchange} functions. */ 45 45 export interface OperationContext { 46 46 [key: string]: any; 47 + additionalTypenames?: string[]; 47 48 fetchOptions?: RequestInit | (() => RequestInit); 48 49 requestPolicy: RequestPolicy; 49 50 url: string;