Subscribe on changes!

`shallowReactive` collections (Set & Map) incorrectly unproxy added items without re-proxying them on retrieval, leading to identity hazards

avatar
Jun 27th 2023

Vue version

@9f8e98a

Link to minimal reproduction

https://play.vuejs.org/#eNp9UstuwjAQ/JWVLw0SMoeql0CR+uDQHtoKuGEOabKAabAtP4AK5d+7cRpoK8olcmZmZ2fXPrA7Y/g2IEvZwOVWGg8OfTBDoeTGaOvhABaz3MstdsGtsrLUu/E3ABUsrN7AFRlcCSVUrpXzkFmbfcLtX3Uym3f60IqoyxmJwh1M0CedTr8V6vc1CdsMyQGyFG6gIivyEio24ya4VULKuoyceVYU7W/jo0vkpV4mM855LJl3oT6TeN6BXg+mK3QIhVws0EKa/FfFpcrLUKCL9ieP33h0fEQnLRaggzfBp+BtoBXWX6EGvWbZQ5qBdZl31G0hl3zttKKrOAhF07Fcb4ws0b4aLymNYClEpubi3p4jFo1bPF9h/nEGX7t9jQn2ZmlQu0XBjpzP7BJ9Q48mL7in85Hc6CKUpL5AjpE2FeqMjew+qIJi/9DFtE/xQUm1nLrR3qNy7VDNTgCqqBeMntPDhdFPca/5dawTqmLVF8Ba8E8=

Steps to reproduce

Open the console, it should unintuitively log out true false.

What is expected?

It is expected that the two cases behave the same. Adding items to a collection, then retrieving them again using their iterators (what spread uses) should return the items unchanged.

What is actually happening?

The underlying issue is that, compared to arrays, Sets (and I assume Maps too) always un-proxy items added into them.

This is the expected and correct behavior in the case that the Set is reactive, and is also what happens with arrays. In this case, when an object is retrieved, it is automatically wrapped into a Proxy again. In fact, the reproduction provided can be fixed by replacing shallowReactive with reactive.

However, the bug lies in the fact that when the Set is only shallowReactive, the unwrapping still happens on add, but it doesn't wrap again on retrieval. This behavior is divergent from arrays, where pushing into a shallowReactive array simply adds the item unchaged, with the Proxy in tact.

My suggestion: Adding a proxied item into a shallowReactive collection should leave the item untouched and add the proxied version, making it identical to arrays. If this leads to identity hazards and ends up breaking .has, then the alternative solution is to always unwrap the item, however to remember to wrap it again on retrieval - regardless if the collection is deeply or shallowly reactive.

System Info

System:
    OS: macOS 13.3.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 197.44 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 18.14.2 - /opt/homebrew/opt/node@18/bin/node
    Yarn: 1.22.19 - /opt/homebrew/bin/yarn
    npm: 9.5.0 - /opt/homebrew/opt/node@18/bin/npm
  Browsers:
    Chrome: 114.0.5735.198
    Safari: 16.4

Any additional comments?

I managed to isolate this bug after running into many "identity hazards" while developing my application, as some objects where unexpectedly being unproxied.

avatar
Jul 26th 2023

I'm not sure if this is a problem.

from the source code. when u use push to add a value to array. The added value is not changed https://github.com/vuejs/core/blob/3be4e3cbe34b394096210897c1be8deeb6d748d8/packages/reactivity/src/baseHandlers.ts#L75-L84 (toRaw(this) as any)[key].apply(this, args)

but if u use set to add a value . The added value will be toRaw https://github.com/vuejs/core/blob/3be4e3cbe34b394096210897c1be8deeb6d748d8/packages/reactivity/src/collectionHandlers.ts#L69-L79 value = toRaw(value)

so they are different.

avatar
Jul 26th 2023

This is a problem, and it has caused identity issues in my software. Don't see a reason why the proxy behavior for these two structures should diverge to the outside