Reactivity on object properties that are internally updated

Hi everyone.

I’ve been having some issues with reactivity within my Vue (3.0) forms when binding to instances of my custom classes. Specifically, my problems seem to stem from the fact that the properties of my class are updated from within the class and this does not trigger updates in the DOM.

See this basic example (Test.vue):

<template>
  <label>My Object: {{ myObj.myProp }}</label>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";

class MyClass {
  public myProp: number = 0;

  constructor() {
    setInterval(() => {
      this.myProp++;
    }, 1000);
  }
}

export default defineComponent({
  name: "Home",
  setup() {
    const myObj = reactive(new MyClass());
    
    return {
      myObj,
    };
  },
});
</script>

In the above example, you can see there is a class definition “MyClass” that contains a public prop “myProp”. In the class constructor, I set up an interval that will increment the value of the prop each second. Within my component, I create an instance of my class, wrapping it with the Vue 3.0 “reactive” method before returning it. I then bind to the prop in the Vue template.

When the page loads, the value “My Object: 0” is rendered in a label. When debugging, it can be seen that the interval is indeed firing and the value of the prop is incrementing each second but the label does not update.

My understanding is that when wrapping the class instance with “reactive”, a “proxy” is made that will add reactivity to all properties but only when accessed/mutated via the proxy (in my example that would be via “myObj”). When the prop is updated internally, it does not go through this “proxy” and as such, does not trigger any updates to the DOM.

Is my understanding here correct?

I found documentation (related to Vue 2.x) that suggested that when reactivity does not work as expected, a method can be used to trigger updates manually:

Vue.forceUpdate();
// or
vm.$forceUpdate();

Unfortunately I’ve not been able to find any documentation on its use with Vue 3.0. However, I was able to get something similar to work.

Updated example (Test.vue):

<template>
  <label>My Object: {{ myObj.myProp }}</label>
</template>
<script lang="ts">
import { defineComponent, getCurrentInstance, reactive } from "vue";

class MyClass {
  private _myProp: number = 0;
  private ctx: any;

  constructor(ctx: any) {
    this.ctx = ctx;

    setInterval(() => {
      this.myProp++;
    }, 1000);
  }

  get myProp() {
    return this._myProp;
  }

  set myProp(val) {
    this._myProp = val;
    if (this.ctx.update) {
      this.ctx.update();
    }
  }
}

export default defineComponent({
  name: "Home",
  setup() {
    const myObj = reactive(new MyClass(getCurrentInstance()!));
    
    return {
      myObj,
    };
  },
});
</script>

In my updated example above, I use the Vue API method “getCurrentInstance” to grab a reference to the current component context. I pass this through to my class constructor and reorganise my prop setter so that it runs the update() method on the component context whenever the property updates. This works and when the form loads, the label updates each second as the interval fires.

My questions regarding this are:

  1. Given my situation (self updating objects), is this an acceptable solution or is there a better way forward?
  2. Are there any foreseeable performance issues with this approach? My understanding is that Vue batches updates so hopefully this approach is scalable. I expect to have several different class instances on a form, each with a dozen or so properties updating once a second.
  3. I could not find documentation regarding the update method. Is this the correct use of this method?
  4. Is the update method meant to be publicly available or could it be removed in the future?

Thank you in advance for your time and help.

1 Like

It sounds right to me.

$forceUpdate does still exist in Vue 3 but you won’t have access to it within setup.

Anything involving forcing updates or getCurrentInstance should be regarded as a last resort. I think there’s a less dramatic way to fix it (the code below is JavaScript but the equivalent should work in TypeScript):

class MyClass {
  constructor() {
    this.myProp = 0    
  }
  
  start () {
    setInterval(() => {
      this.myProp++
    }, 1000)
  }
}

// Inside setup...
const myObj = reactive(new MyClass())
myObj.start()

Calling the start method via the proxy allows Vue to get involved. The this inside start will be the proxy, so all updates go through the proxy.

1 Like

Thanks skirtle for the advice. Your suggested solution worked as expected and I was able to apply it to our application framework successfully. :smiley: