Subscribe on changes!

Error when property value is an instance of a class using private class fields

avatar
Apr 24th 2023

Vue version

3.2.47

Link to minimal reproduction

https://codepen.io/leaverou/pen/MWPpEQg?editors=1011

Steps to reproduce

Visit testcase

What is expected?

1 in Result pane

What is actually happening?

Error

System Info

(Not relevant)

Any additional comments?

Apparently proxies break private class fields (MDN, non-Vue testcase). This means that every time an instance of a class using private fields is used as the value of a Vue property, things break (see testcase). Since there is a workaround that can be used in the proxy traps, I'd consider this a Vue bug.

Vue was mentioned several times in https://github.com/tc39/proposal-class-fields/issues/106 , so I'd be surprised if this is not known, but I couldn't find an existing issue, so opening this just in case.

avatar
Apr 24th 2023
import { createApp, shallowRef, toRaw, markRaw } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

class Foo {
    #bar = 1;
    
    get bar() {
        return this.#bar;
    }
}

createApp({
    data() {
        return {
            foo: shallowRef(new Foo()) // or toRaw, but it's loses reactivity 🫠
        }
    },
    mounted() {
        console.log(this.foo)
    }
}).mount("#app");

Vue transfer foo into Proxy Object so this keyword will refers to Proxy Object instead of normal Object

Option API:

Proxy { foo: object }

Composition API:

Proxy { foo: RefImpl }
avatar
Apr 24th 2023

#2981

avatar
Apr 25th 2023

In order for Vue to properly track property access inside object / class instance methods, this inside these methods must also be the wrapped proxy. However, this being the proxy instead of the original object prevents us from reading private fields.

If we use the original object as this inside methods, then it would break reactivity tracking for non-private properties, which is the more common case. In other words, there isn't a proper fix for this, so it's a hard limitation based on the way Vue's reactivity system works.

In general, we recommend using plain objects over class instances as data sources. If you really need encapsulation and only expose certain reactive state, consider using Composition API and using Composables.

avatar
Jul 17th 2023

Link to minimal reproduction

https://codepen.io/leaverou/pen/MWPpEQg?editors=1011

What is expected?

1 in Result pane

Any additional comments?

Apparently proxies break private class fields (MDN, non-Vue testcase). This means that every time an instance of a class using private fields is used as the value of a Vue property, things break (see testcase). Since there is a workaround that can be used in the proxy traps, I'd consider this a Vue bug.

Vue was mentioned several times in tc39/proposal-class-fields#106 , so I'd be surprised if this is not known, but I couldn't find an existing issue, so opening this just in case.

I assume you need immutable object properties (private ones in JS case) for well-known reasons. There is a workaround for Vue. In the private property accessor (a JS getter) unwrap the proxy and access the property on the raw object. For your codepen the update would be:

import { createApp, isProxy, toRaw } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
        // ...

    get bar() {
        return isProxy(this) ? toRaw(this).#bar : null; // Just an example. Treat the values as per concrete use case.
    }

This works with both Options and Composition APIs.

Since there is a workaround that can be used in the proxy traps, I'd consider this a Vue bug.

@LeaVerou Can you clarify what workaround you meant here. Thanks.

avatar
Jul 17th 2023

@WhereJuly Your workaround requires coupling Vue with the class using the private properties, however these may be developed entirely separately. E.g. in my case, I was handling Color objects with Vue, and Color.js is a library that has nothing to do with Vue whatsoever (I’ve since filed an issue and it moved away from private properties, but not all libraries can afford to do that).

avatar
Jul 18th 2023

@LeaVerou True, my workaround requires coupling with Vue. For code assumed to work along with Vue it is fine, though for code trying to be Vue-agnostic this is the problem. Could be probably solved by some Vue-aware adapter object nearby the original object to still keep properties private. Though this approach applicability and usefulness depends a lot on a paricular use case.

However in your initial post you mentioned there is the other workaround that can be used in the proxy traps. It sounds really intersting. Could you explain what it is or give a reference? Thanks.