5

When I create a ref from an empty object and later add object properties, there is no reactivity:

<template>
  <p>{{hello}}</p>
  <button @click="add()">Click me</button>
</template>

<script>
import {ref} from 'vue';
export default {
  name: "Test",
  setup(){
    const myData = ref({});
    return {myData}
  },
  methods: {
    add(){
      this.myData["text"] = "Hello";
      console.log(this.myData);
    }
  },
  computed: {
    hello(){
      return this.myData.hasOwnProperty("text")) ? this.myData["text"] : "no text";
    }
  }
}
</script>

Clicking the button shows that myData has changed but the computed property hello does not update.

Also tried reactive({}) instead of ref({}) without success.

It works when we initialize the ref with properties, like const myData = ref({"text": "no text"});.

But why does the empty object not work?

EDIT:
Finally found out what exactly the problem is and how it can be solved: The reactivity core of Vue3 is not alert of Object.keys() but only of the values of the properties, and the empty object does not have any. However, you can make Vue3 alert, if the computed property is defined like

computed: {
    hello(){
      return Object.keys(this.myData).indexOf("text") > -1 ? this.myData["text"] : "no text";
    }

The call to Object.keys(this.myData) is needed to make the reactivity system aware of what we are interested in. This is similar to setting a watch on Object.keys(this.myData) instead of watching this.myData.

Nechoj
  • 1,512
  • 1
  • 8
  • 18

2 Answers2

2

Try to you update your ref object like

this.myData = {"text": "Hello"}

const { ref, computed } = Vue
const app = Vue.createApp({
  /*setup(){
    const myData = ref({});
    const hello = computed(() => myData.value.hasOwnProperty("text") ? myData.value.text : myData.value = "no text")
    const add = () => {
      if(Object.keys(myData.value).length === 0) {
        myData.value = {'text': "Hello"};
      } else {
        myData.value.otherProperty = "Hello again"
      }
    }
    return { myData, add, hello }
  },*/
  setup(){
    const myData = ref({});
    return { myData }
  },
  methods: {
    add(){
      if(Object.keys(this.myData).length === 0) {
        this.myData = {"text": "Hello"}
      } else {
        this.myData.otherProperty = "Hello again"
      }
      console.log(this.myData)
    },
  },
  computed: {
    hello(){
      return Object.keys(this.myData).length !== 0 ? this.myData[Object.keys(this.myData)[Object.keys(this.myData).length - 1]] : "no text"
    }
  }
})
app.mount('#demo')
<script src="https://unpkg.com/vue@3.2.29/dist/vue.global.prod.js"></script>
<div id="demo">
  <p>{{ hello }}</p>
  <button @click="add">Click me 2 times</button>
</div>
Nikola Pavicevic
  • 21,952
  • 9
  • 25
  • 46
  • Thanks for the good answer. But I want to add properties to the object, not replacing it. In my application more and more properties are added by different components (actually, the ref object is in a pinia store). So I need a way to update it. – Nechoj Mar 13 '22 at 21:12
  • @Nechoj hey mate, I updated answer take a look please :) btw maybe not to mix options and composition API – Nikola Pavicevic Mar 13 '22 at 21:19
  • So you're saying first I need replacement and later can add another prop? But if I click the second button first I don't get `otherProperty` but still first property. It's the same as clicking the first button twice, right? (Note: I mixed composition and options API to test `reactive` within setup, which I don't know how to do it in options ...) – Nechoj Mar 13 '22 at 21:26
  • Yes you are right, sorry I don't understand completely what you are trying to achieve – Nikola Pavicevic Mar 13 '22 at 21:32
  • Goal: Adding properties to a ref object and make component react when a property has been added. The object grows and the App get's notified. Some have proposed to add a counter property to the object that is being increased whenever something has been added. But I thought, there must be a clean way of doing this. – Nechoj Mar 13 '22 at 21:36
  • Ok, please check again, maybe now I got you :) – Nikola Pavicevic Mar 13 '22 at 22:08
  • Great, I think I adapt your solution to my problem. Thanks! – Nechoj Mar 14 '22 at 08:44
0

If you change your computed property to be defined such that it references myData['text'] directly before returning, things work as expected:

  computed: {
    hello() {
      return this.myData['text'] || 'no text'; // works
    }

I suspect what's going on with your original code is that the Vue dependency-tracking code is not able to see that your function depends on myData. Consider that hello is being called (by Vue) before the text property exists on the object. In that case, the function returns before actually touching the proxied value (it short-circuits as soon as it sees that hasOwnProperty has returned false).

Dependency tracking in Vue is done dynamically, so if your computed property doesn't touch any reactive variables when called, Vue doesn't see it as having any external dependencies, and so won't bother calling it in the future. It will just use the previously-cached value for subsequent calls.

Myk Willis
  • 12,306
  • 4
  • 45
  • 62
  • Thanks! The point is to add properties that are not known when initialialising the ref. That's why it is empty at the beginning. When we initialize like `const myData = ref({"text": "no text"});` it will work. The question is why Vue does not notice a change in the object's properties. Is this an intended behavior? – Nechoj Mar 14 '22 at 08:32
  • 1
    @Nechoj the reason Vue doesn't notice the change is because the dependency-tracking code is not able to determine that `hello()` depends on `myData` (because when `hello()` is called initially, it doesn't depend on it). It's not intentional per se, but I don't think this pattern is common or contemplated by Vue. Definitely better off populating all properties initially, and having simple computed properties that reference them. (this answer may be helpful: https://stackoverflow.com/a/51699388/925478 ) – Myk Willis Mar 14 '22 at 14:04
  • Thanks for the explanation and the link. It helps to understand that there are limitations to the reactivity. What is a bit surprising to me is that Vue observes values of Object properties only, not the list of properties (= Object.keys()). Maybe, as you suggest, this use case was not in the focus yet. The reason why I was trying it is because my app is depenedent and connected to other systems and I needed a way to keep track of the information coming in over the interfaces. At the time of programming the component, I did not want to fix the properties of the ref store and keep it flexible. – Nechoj Mar 14 '22 at 14:23