Vue 3 reactivity not triggered from inside a class instance
Asked Answered
A

2

14

Codepen: https://codepen.io/codingkiwi_/pen/XWMBRpW

Lets say you have a class:

class MyClass {
  constructor(){
    this.entries = ["a"];

    //=== example change triggered from INSIDE the class ===
    setTimeout(() => {
      this.entries.push("c");
    }, 1000);
  }
}

And in a component you get an instance of that class:

const { reactive } = Vue;

const App = {
  setup() {
    const myobject = reactive(new MyClass());

    //=== example change triggered from OUTSIDE the class ===
    setTimeout(() => {
      myobject.entries.push("b");
    }, 500);
    
    return {
      myobject
    };
  }
}

The myobject.entries array in the DOM will display the entries "a" and "b", but not "c"

Amoakuh answered 8/6, 2021 at 20:55 Comment(0)
A
4

The myobject.entries array in the DOM will display the entries "a" and "b", but not "c"

This is because "a" was already in the array as we made the instance reactive and the push of "b" happened from outside the object, through the Proxy.

To be clear: the const myobject does not contain the MyClass instance, it contains a Proxy of the instance which handles/wraps the original instance! And it is the proxy which is passed to the DOM / Template.

The push of "c" happened from inside the object, not through the proxy. So "c" will be pushed to the array, but no reactivity change will be triggered.

Fix:

Mark the array that we change from inside the class explicitly as reactive like this:

class MyClass {
  constructor(){
    this.entries = reactive(["a"]);

    //=== example change triggered from INSIDE the class ===
    setTimeout(() => {
      this.entries.push("c");
    }, 1000);
  }
}

Alternatively try to only work with the proxied object like the docs suggest:

The best practice here is to never hold a reference to the original raw object and only work with the reactive version:

Docs: https://v3.vuejs.org/guide/reactivity.html#proxy-vs-original-identity

Amoakuh answered 8/6, 2021 at 20:55 Comment(0)
H
13

As another answer explains, reactive creates proxy object to enable the reactivity. this in constructor refers to original MyClass instance and not a proxy, so it cannot cannot be reactive.

This indicates the probem in the code. reactive takes into account synchronous operations that occur in MyClass constructor. It's an antipattern to perform asynchronous side effects in a constructor , the reasons include possible implications with the code that consumes such constructor.

This can be solved with:

class MyClass {
  constructor(){
    // synchronous stuff only
    this.entries = ["a"];
  }

  init() {
    // asynchronous stuff, probably return a promise
    setTimeout(() => {
      this.entries.push("c");
    }, 1000);
  }
}

and

const myobject = reactive(new MyClass());
myobject.init() // `this` is a proxy inside `init`
Handwork answered 8/6, 2021 at 22:20 Comment(2)
Thank you! The antipattern in the constructor was actually just in the example :) In the real code I encountered this problem I defined a listener function in the constructor this.listener = () => { this.entities.push(); } (to be able to unsubscribe it later) and so, like you explained, "this" in the listener function referred to the original instanceAmoakuh
In this case the workaround is to not use arrow methods and lazily bind methods, e.g. listener is a regular method and is used as event => this.listener(event) or this.listener.bind(this). IIRC this is something that github.com/NoHomey/bind-decorator decorator can do, in case your setup supports them. The class shouldn't necessarily have hard-coded reactive to be used inside Vue component as another post suggests, but it needs to be designed with this concern in mind.Handwork
A
4

The myobject.entries array in the DOM will display the entries "a" and "b", but not "c"

This is because "a" was already in the array as we made the instance reactive and the push of "b" happened from outside the object, through the Proxy.

To be clear: the const myobject does not contain the MyClass instance, it contains a Proxy of the instance which handles/wraps the original instance! And it is the proxy which is passed to the DOM / Template.

The push of "c" happened from inside the object, not through the proxy. So "c" will be pushed to the array, but no reactivity change will be triggered.

Fix:

Mark the array that we change from inside the class explicitly as reactive like this:

class MyClass {
  constructor(){
    this.entries = reactive(["a"]);

    //=== example change triggered from INSIDE the class ===
    setTimeout(() => {
      this.entries.push("c");
    }, 1000);
  }
}

Alternatively try to only work with the proxied object like the docs suggest:

The best practice here is to never hold a reference to the original raw object and only work with the reactive version:

Docs: https://v3.vuejs.org/guide/reactivity.html#proxy-vs-original-identity

Amoakuh answered 8/6, 2021 at 20:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.