sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
77.54k stars 4.05k forks source link

$state() of Array : incorrect behavior of indexOf()/findIndex() with object #11556

Open adiguba opened 2 months ago

adiguba commented 2 months ago

Describe the bug

When we used an $state([]), some basic function of Array have an incorrect behavior with object. It seems that all equality operations fail, as the object in the array is a proxy.

    let array = $state([]);

    const obj = { data: "any value" };
    array.push(obj);

    console.log( array[0] === obj );                 // false !!!
    console.log( array.indexOf(obj) );               // -1 !!!
    console.log( array.findIndex(n => n === obj) );  // -1 !!!

It’s really disturbing and not easy to understand...

Reproduction

Exemple in REPL : the buttons addRandomNumber, addRandomString and addRandomObject add a number/string/object in an array, and logs the index.

This works as espected for number and string, but fails for objects.

REPL

Logs

No response

System Info

REPL

Severity

annoyance

longnguyen2004 commented 2 months ago

Maybe use $state.frozen instead? REPL

As I understand it, there's no way to both make an object/array deeply reactive and maintain equality with the original object/array at the same time, since you have to add extra stuff to make it reactive, and you wouldn't want those extra stuff to propagate back to the original object.

trueadm commented 2 months ago

I think this is more a documentation problem. Svelte uses proxies to make deep reactivity work, and thus equality of things you put in state will never match the same thing as what you put in, because they're all wrapped with proxies.

7nik commented 2 months ago

The workaround seems to reactify the searched value: the $state never creates wrappers (not sure how reliable this behaviour is). Basically:

const value = {};
$state(value) === $state(value); // true
$state(value) === $state($state(value)); // true

const arr = $state([ value ]);
arr.includes($state(value)); // true

REPL

The proxy could alter the behaviour of search methods, but it won't resolve cases like arrState[0] == rawObject.

trueadm commented 1 month ago

@dummdidumm I feel like the new warnings largely address this issue now.

dummdidumm commented 1 month ago

There needs to be more docs on this, the warning itself is somewhat vague without any additional explanation IMO. It could come as part of the general "more details on a warning" system we can put in place with how warnings are setup now.

trueadm commented 1 month ago

There needs to be more docs on this, the warning itself is somewhat vague without any additional explanation IMO. It could come as part of the general "more details on a warning" system we can put in place with how warnings are setup now.

It might be worth documenting now whilst it’s fresh in your head. I don’t personally think it’s all that needed so maybe your documentation can help me understand your thought process.

longnguyen2004 commented 1 month ago

This should be addressed by https://github.com/sveltejs/svelte/pull/11613

dm-de commented 1 month ago

This should be addressed by #11613

    console.log( array[0] === obj );                 // false !!!
    console.log( array.indexOf(obj) );               // -1 !!!
    console.log( array.findIndex(n => n === obj) );  // -1 !!!

Here is only for 1st line a solition in Svelte 5. For other two lines - here is nothing, but we can use a for loop.

I'm unhappy about so many extra runes... It looks like React now - solution for self created problems.

trueadm commented 1 month ago

@dm-de It's not a problem if you reference things in state from the beginning. If you don't then you'll need to use $state.is. This is just how JavaScript proxies work, if you'd rather not use them, and you'd rather use immutable objects instead then you have $state.frozen where you don't need to worry about proxies.

adiguba commented 1 month ago

11613 add a partial fix for this, and now these lines will produce a warning state_proxy_equality_mismatch :

items.findIndex(n => n === item);  // WARN state_proxy_equality_mismatch

This is fine, and the warning message is explicit enough :

Reactive $state(...) proxies and the values they proxy have different identities. Because of this, comparisons with === will produce unexpected results. Consider using $state.is(a, b) instead

=> So a === b should be replaced by $state.is(a, b) :

- items.findIndex(n => n === item);
+ items.findIndex(n => $state.is(n, item));

👍

But for indexOf the warning is ambiguous :

items.indexOf(item);  // WARN state_proxy_equality_mismatch

Reactive $state(...) proxies and the values they proxy have different identities. Because of this, comparisons with array.indexOf(...) will produce unexpected results. Consider using $state.is(a, b) instead

=> How do we replace array.indexOf(...) with $state.is(a, b) ???

In fact here we should replace the indexOf() by a findIndex() :

- items.indexOf(item);
+ items.findIndex(n => $state.is(n, item));

So :

In "pseudo-code", the indexOf() of an reactive array should be equivalent to :

indexOf(item) {
   if (item!=null && typeof item === 'object) {
      // item is an object, use findIndex()
      return this.findIndex(n => $state.is(n, item));
   } else {
      return /* original indexOf() */
   }
}
longnguyen2004 commented 1 month ago

I still think Svelte should specifically handle calls to indexOf()/lastIndexOf()

We're getting into monkey-patching territory with this, and I'm pretty sure everyone agrees that we shouldn't do that.