Prototype-inherited properties not reactive in vue.js?

Viewed 476

I have this code. It's purely for experimental purposes :

test.html

<div id="example-1">
    {{ stuff.dosobject.notlist }}
    <button v-on:click="dostuff">DO STUFF</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script src="test.js"></script>

test.js

var oneobject={
        dosobject:{
            notlist:"foo"  
        }
    }

var stuff = Object.create(oneobject)

var example1 = new Vue({
  el: '#example-1',
  data: {
    stuff:stuff
  },
  methods:
  {
      dostuff:function(event)
      {
          if(stuff.dosobject.notlist=="foo")
              stuff.dosobject.notlist="bar"
          else
              stuff.dosobject.notlist="foo"
      }
  }
})

The object stuff is created with oneobject as a prototype. I would expect the property stuff.dosobject.notlist to be reactive, ie the {{ stuff.dosobject.notlist }} part of the template to be re-rendered when the button is clicked.

It doesn't seem to be the case.

Am I doing something wrong ? Or is it normale VueJS behavior?

After thinking a bit about it, It would not seem so weird to not make all prototype properties reactive because the prototype chain can be quite long and cause performance issues.

1 Answers

Normally, the way you'd get around these types of reactivity caveats is to use Vue.set:

Adds a property to a reactive object, ensuring the new property is also reactive, so triggers view updates. This must be used to add new properties to reactive objects, as Vue cannot detect normal property additions (e.g. this.myObject.newProperty = 'hi').

However, Vue.set contains the following check:

https://github.com/vuejs/vue/blob/4f81b5db9ab553ca0abe0706ac55ceb861344330/src/core/observer/index.js#L212-L215

if (key in target && !(key in Object.prototype)) {
 target[key] = val
 return val
}

I've read and re-read Reactivity in Depth, which states:

When you pass a plain JavaScript object to a Vue instance ... (emphasis mine)

So it seems clear you should not be passing complicated objects and expect reactivity to work. However, prototypical inheritance is part of "plain" objects, and when you're working with classes, 3rd party libraries, or large, complicated code-bases, dealing with "non-plain" objects passed as props or data is sometimes unavoidable.

It would be nice to "make reactive" existing objects via use of Vue.set, but because of the in check, it won't copy the prototype value to the object and set up the reactive getter/setter pair on it.

Here are two solutions you can try:

  1. Clone the object instead of inheriting from it, ensuring stuff has dosobject as an own property.

  2. Trigger $forceUpdate when you set any property on stuff, or create a proxy for stuff to do it automatically in a set trap.


In an edge case we encountered, we set up a function to ensure changes we set are picked up by Vue's reactivity system:

function setPropertyReactive(obj, key, value) {
  if (obj.hasOwnProperty(key)) {
    obj[key] = value;
    return;
  }

  delete Object.getPrototypeOf(obj)[key];
  Vue.set(obj, key, value);
}
Related