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.

(graphcache) - Fix previous result's null values spilling into next result (#1885)

* Fix typo in operations/query.ts resolveLink

* Separate input from output data

* Fix skipped layers being ineffective depending on last result

* Add changeset

* Simplify hasChanged in readRoot

* Prevent input data from leaking into resolver context

authored by

Phil Pluckthun and committed by
GitHub
30eb7941 ab7ac600

+35 -22
+5
.changeset/mean-pugs-laugh.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Fix previous results' `null` values spilling into the next result that Graphcache issues, which may prevent updates from being issued until the query is reexecuted. This was affecting any `null` links on data, and any queries that were issued before non-optimistic mutations.
+29 -21
exchanges/graphcache/src/operations/query.ts
··· 66 66 error?: CombinedError | undefined, 67 67 key?: number 68 68 ): QueryResult => { 69 - initDataState('read', store.data, (data && key) || null); 69 + initDataState('read', store.data, key); 70 70 const result = read(store, request, data, error); 71 71 clearDataState(); 72 72 return result; ··· 121 121 ctx: Context, 122 122 entityKey: string, 123 123 select: SelectionSet, 124 - data: Data 124 + input: Data 125 125 ): Data => { 126 - const typename = ctx.store.rootNames[entityKey] ? entityKey : data.__typename; 126 + const typename = ctx.store.rootNames[entityKey] 127 + ? entityKey 128 + : input.__typename; 127 129 if (typeof typename !== 'string') { 128 - return data; 130 + return input; 129 131 } 130 132 131 133 const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx); 132 134 133 135 let node: FieldNode | void; 134 136 let hasChanged = false; 135 - const output = makeData(data); 137 + const output = makeData(input); 136 138 while ((node = iterate())) { 137 139 const fieldAlias = getFieldAlias(node); 138 - const fieldValue = output[fieldAlias]; 140 + const fieldValue = input[fieldAlias]; 139 141 // Add the current alias to the walked path before processing the field's value 140 142 ctx.__internal.path.push(fieldAlias); 141 143 // We temporarily store the data field in here, but undefined ··· 152 154 } 153 155 154 156 // Check for any referential changes in the field's value 155 - hasChanged = hasChanged || dataFieldValue !== output[fieldAlias]; 157 + hasChanged = hasChanged || dataFieldValue !== fieldValue; 156 158 if (dataFieldValue !== undefined) output[fieldAlias] = dataFieldValue!; 157 159 158 160 // After processing the field, remove the current alias from the path again 159 161 ctx.__internal.path.pop(); 160 162 } 161 163 162 - return hasChanged ? output : data; 164 + return hasChanged ? output : input; 163 165 }; 164 166 165 167 const readRootField = ( ··· 258 260 ctx: Context, 259 261 key: string, 260 262 select: SelectionSet, 261 - data: Data, 263 + input: Data, 262 264 result?: Data 263 265 ): Data | undefined => { 264 266 const { store } = ctx; ··· 303 305 304 306 let hasFields = false; 305 307 let hasPartials = false; 306 - let hasChanged = typename !== data.__typename; 308 + let hasChanged = typename !== input.__typename; 307 309 let node: FieldNode | void; 308 - const output = makeData(data); 310 + const output = makeData(input); 309 311 while ((node = iterate()) !== undefined) { 310 312 // Derive the needed data from our node. 311 313 const fieldName = getName(node); ··· 340 342 ) { 341 343 // We have to update the information in context to reflect the info 342 344 // that the resolver will receive 343 - updateContext(ctx, data, typename, entityKey, key, fieldName); 345 + updateContext(ctx, output, typename, entityKey, key, fieldName); 344 346 345 347 // We have a resolver for this field. 346 348 // Prepare the actual fieldValue, so that the resolver can use it ··· 364 366 fieldName, 365 367 key, 366 368 getSelectionSet(node), 367 - output[fieldAlias] as Data, 369 + (output[fieldAlias] !== undefined 370 + ? output[fieldAlias] 371 + : input[fieldAlias]) as Data, 368 372 dataFieldValue, 369 - ownsData(output) 373 + ownsData(input) 370 374 ); 371 375 } 372 376 ··· 390 394 fieldName, 391 395 key, 392 396 getSelectionSet(node), 393 - output[fieldAlias] as Data, 397 + (output[fieldAlias] !== undefined 398 + ? output[fieldAlias] 399 + : input[fieldAlias]) as Data, 394 400 resultValue, 395 - ownsData(output) 401 + ownsData(input) 396 402 ); 397 403 } else { 398 404 // Otherwise we attempt to get the missing field from the cache ··· 405 411 typename, 406 412 fieldName, 407 413 getSelectionSet(node), 408 - output[fieldAlias] as Data, 409 - ownsData(output) 414 + (output[fieldAlias] !== undefined 415 + ? output[fieldAlias] 416 + : input[fieldAlias]) as Data, 417 + ownsData(input) 410 418 ); 411 419 } else if (typeof fieldValue === 'object' && fieldValue !== null) { 412 420 // The entity on the field was invalid but can still be recovered ··· 440 448 // After processing the field, remove the current alias from the path again 441 449 ctx.__internal.path.pop(); 442 450 // Check for any referential changes in the field's value 443 - hasChanged = hasChanged || dataFieldValue !== output[fieldAlias]; 451 + hasChanged = hasChanged || dataFieldValue !== input[fieldAlias]; 444 452 if (dataFieldValue !== undefined) output[fieldAlias] = dataFieldValue; 445 453 } 446 454 ··· 449 457 ? undefined 450 458 : hasChanged 451 459 ? output 452 - : data; 460 + : input; 453 461 }; 454 462 455 463 const resolveResolverResult = ( ··· 564 572 } 565 573 566 574 return hasChanged ? newLink : (prevData as Data[]); 567 - } else if (link === null || (prevData === null && ownsData)) { 575 + } else if (link === null || (prevData === null && skipNull)) { 568 576 return null; 569 577 } 570 578
+1 -1
exchanges/graphcache/src/store/data.ts
··· 92 92 export const initDataState = ( 93 93 operationType: OperationType, 94 94 data: InMemoryData, 95 - layerKey: number | null, 95 + layerKey?: number | null, 96 96 isOptimistic?: boolean 97 97 ) => { 98 98 currentOwnership = new Set();