gqty icon indicating copy to clipboard operation
gqty copied to clipboard

`deepAssign` is mutating the source object and causing cache merging problem

Open ColaFanta opened this issue 4 years ago • 4 comments

Hi guys,

I'm trying to make a blog web app using gqty, but recently I encountered a problem using usePaginatedQuery with fetchPolicy set to 'cache-and-network'. Say I have two pages, a 'Latest Articles' page and a 'Favorite Articles' page, and they have some articles in common. When I disliked an article and refresh the 'Favorite Articles' page then headed back to 'Latest Articles', the content of 'Latest Articles' was affected as well.

After days of debugging, I discovered that the deepAssign function that used for merging cache is somehow mutating data unexpectedly. There is a chance to have cache of 'queryA(List)' mutated when merging incoming data from 'queryB(List)' when their lists both share itemA.

To reproducing this behavior, I drew out the deepAssign function and created this sandbox here. sandbox

I think that the problem is possibly because deepAssign is, under some circumstances, still doing shallow copy instead of deep one. Here I paste the line that causes shallow copy. https://github.com/gqty-dev/gqty/blob/main/packages/gqty/src/Utils/object.ts#L44 if (sourceKey in target) {...} Reflect.set(target, sourceKey, sourceValue); When the below situation happens:

const target= {}
const sourceKey= 'key1'
const sourceValue= {
  foo: {
    bar: "bar"
  }
}

Reflect.set(target, sourceKey, sourceValue)

console.log(target.key1 === sourceValue) // Prints true, meaning they have the reference to the same object.

//---Try mutate the target
target.key1.foo.bar = 213

console.log(sourceValue.foo.bar) // Print 213

The later change on target is still able to affect data in sourceValue. This is because sourceValue didn't get a deep copy before assigning to target.

To fix this, I came up with a simple solution. changing: if (sourceKey in target) {...} Reflect.set(target, sourceKey, sourceValue); to: if (sourceKey in target) {...} Reflect.set(target, sourceKey, isObject(sourceValue) ? deepAssign({}, [sourceValue], onConflict): sourceValue);

I hope this issue was made clear and my simple solution helps.

ColaFanta avatar Oct 06 '21 08:10 ColaFanta

When I disliked an article and refresh the 'Favorite Articles' page then headed back to 'Latest Articles', the content of 'Latest Articles' was affected as well.

This is part of what normalization is, and is intended behavior, you can customize the normalization https://gqty.dev/docs/client/config#normalization, for example, turning off the normalization for specific object types

PabloSzx avatar Oct 06 '21 15:10 PabloSzx

Hi, I reproduced the unexpected behaviour and please have a look at the below gif:

This is made from a forked CodeSandBox example from the gqty doc with 'Latest Posts', 'Delete Post' added. RPReplay_Final1633551298 Here is what happened and please be aware of the id: 24, title: TestPost12 in the 'Latest Posts':

  1. There were only 5 posts in 'Latest Posts' page as well as in database
  2. Then 'My Posts' page was reached, in which user owned posts were loaded
  3. User deleted the post id: 24, title: TestPost12, and this action ought to reduce number of posts in 'Latest Posts' to 4
  4. Return to page 'Latest Posts', and the unexpected thing happened: id:20, title: TestPost11 became one of the 'Latest Posts'. However, post with id: 20 does for sure not exist in 'Latest Posts' database.

The forked the CodeSandBox is below and the bug can be reproduced here. codesandbox(Please login as [email protected], I made some test data there.)

I hope this would help clarify the issue.

ColaFanta avatar Oct 06 '21 21:10 ColaFanta

@ColaFanta Can you please try using [email protected], it's pretty much doing what you mentioned:

image

PabloSzx avatar Oct 06 '21 23:10 PabloSzx

Sure, I'll try. Thanks a lot~ 👍

ColaFanta avatar Oct 07 '21 04:10 ColaFanta

Please reopen and add more context if this is still an issue.

vicary avatar Dec 04 '22 18:12 vicary