docs icon indicating copy to clipboard operation
docs copied to clipboard

Reactivity: Document that class instances using native private fields are incompatible with ES6 proxies and therefore Vue's reactivity

Open LinusBorg opened this issue 3 years ago • 5 comments

Issue triggered by this issue in core: https://github.com/vuejs/core/issues/7240

A class such as this one will throw an error when the getter is accessed on a reactive proxy for it:

class X {
  #a = 'secret'

  get getA() { return this.#a }
}

const x = reactive(new X())
x.getA // throws

Seee the example in the linked issue for a demo.

LinusBorg avatar Nov 29 '22 15:11 LinusBorg

But this works

class X {
  #a = 'secret'

  get getA() { return this.#a }
}
const p = new Proxy(new X(), {
  get(target, propname, receiver) {
    return Reflect.get(target, propname, target);
  }
});
console.log(p.getA);

Worldwidebrine avatar Nov 29 '22 17:11 Worldwidebrine

that's ...interesting. I need to look up what the specific (but necessary) circumstances were that made this break in our implementation, can't recall.

LinusBorg avatar Nov 29 '22 17:11 LinusBorg

As far as I know, Proxy can't hook method call proxy.method1();. Proxy can only trap the property getter for the method proxy.method1. But getter and setter are not methods. They are called internally not externally. Reflect.get() and Reflect.set() work perfectly.

Worldwidebrine avatar Nov 29 '22 17:11 Worldwidebrine

Ah, you switched out the receiver for the target. That's not the usual use for Reflect, but maybe we can use this to work around this problem in a way.

Edit: on second thought, I don't think this is a path to a solution as we need receiverto be the proxy so we can track other possible property reads in getters.

LinusBorg avatar Nov 29 '22 17:11 LinusBorg

You can save the reactivity for calculated properties like this

import { Ref, ref } from "vue";

class OldClass {
  val: number;
  constructor(){
    setInterval(() => this.val = Math.random(), 1000);
  }

  get my_value(){
    return this.val;
  }
}

const store: Record<string, Ref<any>> = {};
class MyClass extends OldClass {
  vue: Record<keyof OldClass, Ref<any>>;
  constructor() {
    super();
    //@ts-ignore
    this.vue = new Proxy(this, {
      get(target, propname, receiver) {
        const d = Reflect.get(target, propname, target);
        if (typeof propname != "string") return d;
        if (!store[propname]) store[propname] = ref(d);
        if (store[propname].value != d) store[propname].value = d;
        return store[propname];
      },
    });
  }
}

in vue

const my = new MyClass() 
// ...
<div>{{ my.vue.my_value.value }}</div>
<!-- value is reactive! -->

LexSerest avatar Jun 12 '24 21:06 LexSerest