-
-
Notifications
You must be signed in to change notification settings - Fork 974
Reconciling multiple stores with the same object leads to unexpected behaviour and unwanted side effects #2486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Because Stores work by mutation and we don't clone everything we are going to hit stuff like this. It would incredibly unperformant to do that as default for all the other use cases you don't want to do this. And you lose all ability to do any sort of shallow equality checks if you are cloning all the time. Like you could never even diff properly a second time because it wouldn't be the same object that was in the source you were diffing against previously even if it hasn't changed. It is basically just untenable. Immutable internals are another approach but also a waste for the general case so I've needed to figure out if there is a middle ground. Which I have for 2.0 I think. I've been working on a version of reconcile that doesn't mutate. There is a detachment from the reactivity of stores and the underlying object so they are swappable in this If you are modifying things given to stores well they will be modified. It is fundamental. The same object in multiple locations is the same object from a store perspective and should be trackable no matter the path you take to get to it. Which differs from immutable data stores found in something like Redux. So there is only so much that can be done here. |
@ryansolid I see and understand this is a user produced issue because of the udnerlying design. We cannot really touch the underlying object of a store with another peace of code (like reconcile in a different store) that mutates it. It was jsut a source of confusion to me for a bit to pin it down. @mizulu thanks for a potential solution, but the code was written to point something out and the use case is different from using a derived state like you did and a reconciliation where you do not want to change the reference to something but just mutate it in-place |
@JaielZeus, I have put that just in case, as an alternative to derive state, instead of syncing stores. I also looked a little more into the issue. basically an object that is used(shared) in 2 different stores will change in both stores. let o1 = {}
let store1 = {a:o1}
let store2 = {b:o1}
o1.value = "test"
console.log( JSON.stringify(store1) ) // {a:{value:"test}}
console.log( JSON.stringify(store1) ) // {b:{value:"test}} same thing with solid stores let objA = { value: "A" };
let objB = { value: "B" };
let objC = { value: "C" }; let [storeA, setStoreA] = createStore([objA, objB]);
let [storeB, setStoreB] = createStore([objA, objB]); setStoreB(reconcile([objC,objC])); After the reconcile above both This is because the reconcile mutate the raw objects Which is why in your example you see unexpected values, basically your "derived list" So this explain they why |
Now I have looked in the docs of ![]() and then I wondered why in this example, both objA and objB are mutated in the array instead of replaced ?
Another "issue" we can see is that even if we do the following ( unlike the case above, we simply re order) but the reconcile will still mutate let [storeB, setStoreB] = createStore([objA, objB]);
setStoreB(reconcile([objB,objA])); I don't know if reconcile should have just replaced the object at 0 with |
one more way to fix this, is keying the object ( id: ? ) which will also fix this case let objA = {value:"A"}
let objB = {value:"B"}
let [storeB, setStoreB] = createStore([objA, objB]);
console.log(storeB) // [{value"A"}, {value:"B"}]
setStoreB(reconcile([objB,objA]));
console.log(storeB) // [{value"B"}, {value:"B"}] let objA = {id:1,value:"A"}
let objB = {id:2,value:"B"}
let [storeB, setStoreB] = createStore([objA, objB]);
console.log(storeB) // [{id:1,value"A"}, {id:2,value:"B"}]
setStoreB(reconcile([objB,objA]));
console.log(storeB) // [{id:2,value"B"}, {id:1,value:"A"}] and the original issue case baseList.push({ value: current, id: current }); // <- FIX: add id |
@mizulu as you say in the example I provided Is there some memory leak going on by chance with reconcile? Are there multiple Also a peculiar thing is if you do |
let say we limit from -6 to -3 for simplification after adding 3 elements this is how the stores will look like
on the 4th
|
storedList |
---|
o1, o2, o3, o4 |
but derived list is still as below, because it will always reconcile 3 elements into derivedList
it keep the same object o1,o2,o3
here after, and just mutate the,
derivedList |
---|
o1, o2, o3 |
(the last 3 elements now are o2,o3,o4) and so
- o1 is mutated to o2 value
- o2 is mutated to o3 value
- o3 is mutated to o4 value
derivedList
become from 1,2,3
into 2,3,4
and because we modify the o1,o2,o3
storedList
become from 1,2,3,4 to 2,3,4,4
on the 5th add()
the next add reconcile derivedList
again and just mutate o1,o2,o3
(the last 3 elements now are o3,o4,o5) and so
storedList
will transform from 1,2,3,4,5 into 3,4,5,4,5
regarding the .reverse()
case https://playground.solidjs.com/anonymous/ba13d914-e082-40e3-983d-de522450f6a6
it follows the same idea, as described for the case above. but here only the first element is shared
step 1
storedList |
---|
o1 |
derivedList |
---|
o1 |
on step 2
you take the deep clone
c1, c2
when you reconcile c1,c2
into storedList
only the c2
is added as the 2nd element
c1
is just used to shape o1
storedList |
---|
o1[1], c2[2] |
when you try to reconcile the reverse c2,c1
into derivedList
o1
, takes the shape of c2
.
and now the 2nd element is actually c1 which is not shared with storedList
derivedList |
---|
o1[2], c1[1] |
so the stored ending up only sharing the first element o1,
@mizulu ok got it thank you.I wasnt sure but that was my initial gut feeling too because of the output. The other thing is: isn't o1, o2 etc after every click and reconciliation a reference to an object from the deep copied |
👍 I hope my explanation was helpful and not to hard to follow.
generally the answer is no. you can have a 1 million objects in an array If something is held in memory, this will be considered a memory leak. I am still curious to hear from @ryansolid about some of the points from #2486 (comment) |
@mizulu not saying you are wrong and I want to believe you that this is always the case but I doubt for example chrome garbage collects those arrays for some reason. I had some issues when experimenting with weak maps. There in chrome it should, theoretically, remove a reference key and it's value when the underlying object that is used as a key has been freed and nothing but the weakmap is holding the reference of it as a key. Unfortunately I found out that chrome apparently garbage collects sporadically whenever it feels like it. But back to the "memory leak" I will keep in mind that in general the whole array is not kept in memory. It makes sense. Thankss for clarifying everything that I struggled with to accept/understand :) (and your explanation was concise and to the point) |
if you are investigating suspected memory leaks it also provide a way to Collect Garbage
that is a good assumption to make, once the add() call is done leaks by the engine itself, are even less likely but may still be possible |
Describe the bug
When using reconcile to set a store object and reusing the object given to reconcile to set another store object, the code breaks in an unexpected manner. Reusing the same object as a base for reconciliation on two different stores leads to some entanglement that is unexpected.
baseList
object which serves as the data sourcesstoredList
and aderivedList
store object, having different use cases to accomplish.storedList
store object there will beListEntry
s put in as{ value: number; }
objects.derivedList
store object are the last 6 entries of thestoredList
current
and puts a new list entry intobaseList
baseList
is deep copiedstoredList
store objectderivedList
store objectAfter the reconciliation of
derivedList
, thestoredList
store object is mutated as well, which is unexpected to me.I have found, and commented it in the code, 2 possible fixes to this constellation. It seems the reusage of the same underlying object leads to this behaviour when utilizing
reconcile
.I have read
reconcile
is gonna get a rework in the 2.0 version. Will this still be a bug in the new version?Your Example Website or App
https://playground.solidjs.com/anonymous/41a51eb9-de0c-4293-a35c-2ce85c6be36b
Steps to Reproduce the Bug or Issue
Expected behavior
The console should put out an array with integers in order, like
[1,2,3,4,...]
but it puts out wrong values for the first 6 elements after 6 clicks.Screenshots or Videos
Expected output (after 1 of the 2 fixes in comments applied):
Wrong output:
Code
Platform
Additional context
No response
The text was updated successfully, but these errors were encountered: