Vue: newValue and oldValue parameters are the same when deep watching an object

Created on 14 Jan 2016  路  22Comments  路  Source: vuejs/vue

I have a component with these options:

{
  data() {
    return {
      ...
      controls: {
        ...
        showProduct: {
          11: true,
          13: true,
          15: false
        }
      }
    }
  },

  watch: {
    ...
    'controls.showProduct': {
      handler(newValue, oldValue) { // some function },
      deep: true
    }
  }
}

Whenever I change the value of any product (i.e. controls.showProduct[ x ]) the watcher fires, but newValue and oldValue are always the same with controls.showProduct[ x ] set to the new value.

Most helpful comment

@aidangarza
@darkomenz

If you need to watch the entire model, make the entire model a computed property and just use Object.assign({}, model) like the following:

var vm = new Vue({
    el: "#app",
    data: {
        model: {
            title: null,
            description: null,
            id: null,
            createdDate: null
        },
        message: null
    },
    computed: {
        // watch the entire as a new object
        computedModel: function() {
            return Object.assign({}, this.model);
        }
    },
    watch: {
        computedModel: {
            deep: true,
            handler: function(n, o) {
                var message = "";

                if (n.title !== o.title) {
                    message += "<p>Title updated from \"" + o.title + "\" to \"" + n.title + "\"</p>"
                }
                if (n.description !== o.description) {
                    message += "<p>Description updated from \"" + o.description + "\" to \"" + n.description + "\"</p>"
                }

                this.message = message;
            }
        }
    }
});

Computed properties are simply cached values returned from functions so since we are overwriting the cached object of the computed property with a brand new object rather than mutating a property within the original object, we are able to achieve the results we want. Noted: Object.assign polyfill required for some browsers.

Extended example here: http://jsfiddle.net/1fw0357q/

All 22 comments

See the note in api docs: http://vuejs.org/api/#vm-watch

There it is... plain as day. I'm sorry that I missed that. Excellent work on this platform. It's been an absolute pleasure to build with it.

So... is no ways more to detect what part of object changed inside watch? real target path like third param will be useful option... "controls.showProduct.15"

Agreed. It would be nice to be able to tell what changed in the array.

if you want to push to watching array for instance, you can copy this array, then push to this copy, and then assign this copy to your watching array back. this is very simple.

   // copy array which you want to watch
  let copiedArray = this.originArray.slice(0)

  // push to this copy
  copiedArray.push("new value");

  // assign this copy to your watching array
  this.originArray = copiedArray;

@uncleGena explain a little more please, it could help many of us

@BernardMarieOnzo according to docs:

Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn鈥檛 keep a copy of the pre-mutate value.

So what's @uncleGena doing to get the old value, is:

Creating a copy of the existing array to another variable, this will create a new value reference in memory
let copiedArray = this.originArray.slice(0)

Whatever you wanna add to the original array you had to the new variable you just created instead
copiedArray.push("new value");

Now you replace the old array with the new one, replacing the reference in memory from this.originArray to copiedArray, thus Vue will have both references of both variables and you will be able to get both new and old value.
this.originArray = copiedArray;

For a more clear understanding of why it works like this:
Basically when saving a variable object or array, Javascript doesn't keep the record of the value itself, instead it keeps a reference to its location in the Memory, so when you change an array or a object you just update the value in memory but your old and new value of this variable will be the same (the reference to its memory location)

thank you @dynalz and @uncleGena

I'm trying to watch changes that occur based on input fields so the workaround suggested doesn't apply. It would be really nice if this limitation was removed from the API, so the newValue and oldValue properties reflect mutations on bound objects.

As a example:

I have a list of nested tables (https://vuetifyjs.com/en/components/data-tables) that each have inline editable properties (https://vuetifyjs.com/en/components/data-tables#example-editdialog).

I should be able to watch my root array with deep set to true and diff for changes.

im facing exactly the same issue as @darkomenz .... :(
i need to add a new item in the array based on previous selection.

I'm also facing the issue with nested properties changed by input so the workaround isn't applicable. It's a large limitation of Vue to supply the old values. I will have to setup a cached value myself and refer back to that - what a puke-inducing hack. Other less-than-ideal alternative workarounds:

  • Refactor my entire app so each object property is nested inside a child component
  • Add on-change event handler to every input (I have 27 inputs in total)

Anyone have any better ideas?

@ljelewis
If you have all the data in an object (like formData), you may use dynamic watch to watch all 27 inputs in your case.
You can add all inputs in your form to watchers after they are initialized.
e.g.
for (let k in this.formData) { this.$watch('formData.' + k, function (val, oldVal) { console.log(k, val, oldVal) }) }

@aidangarza
@darkomenz

For this problem where it's only one or two properties that I need to watch, I used a computed property along with watch. Something like the following works:

var vm = new Vue({
    el: "#app",
    data: {
        model: {
            title: null,
            description: null,
            id: null,
            createdDate: null
        },
        message: null
    },
    computed: {
        // I just want to watch title for now
        title: function() {
            return this.model.title;
        }
    },
    watch: {
        title: function(n, o) {
            if (n !== o) {
                this.message = "Title updated from \"" + o + "\" to \"" + n + "\""
            }
        }
    }
});

And also, if you are using v-model on your input but are frustrated because it changes on each input event, change your declaration to v-model.lazy. This fires after a change event rather than input.

Extended example on JSFiddle: http://jsfiddle.net/54emch61/

@aidangarza
@darkomenz

If you need to watch the entire model, make the entire model a computed property and just use Object.assign({}, model) like the following:

var vm = new Vue({
    el: "#app",
    data: {
        model: {
            title: null,
            description: null,
            id: null,
            createdDate: null
        },
        message: null
    },
    computed: {
        // watch the entire as a new object
        computedModel: function() {
            return Object.assign({}, this.model);
        }
    },
    watch: {
        computedModel: {
            deep: true,
            handler: function(n, o) {
                var message = "";

                if (n.title !== o.title) {
                    message += "<p>Title updated from \"" + o.title + "\" to \"" + n.title + "\"</p>"
                }
                if (n.description !== o.description) {
                    message += "<p>Description updated from \"" + o.description + "\" to \"" + n.description + "\"</p>"
                }

                this.message = message;
            }
        }
    }
});

Computed properties are simply cached values returned from functions so since we are overwriting the cached object of the computed property with a brand new object rather than mutating a property within the original object, we are able to achieve the results we want. Noted: Object.assign polyfill required for some browsers.

Extended example here: http://jsfiddle.net/1fw0357q/

some how if we use computed state, if we use it with vue and design, the value of input cannot change.
my only solutions right now is to maintain 2 copy of the value, both using object.assign({}, values)
and then compare the new value with the original copy

@someshinyobject You are a star, thank you!

@someshinyobject his solution is great, it didn't worked for me since I had a couple of json objects inside of it. I stringify the object in computed state and parse inside the watcher. This way it simply has to compare a string instead of a whole object.

I did a trick with computed properties, to transform the object into a JSON string, and I watch this string to detect changes. Here is a simple example :

{
    data() {
        return {
            yourObjectOrArray: {}
        }
    },
    computed: {
        yourObjectOrArray_str: function () { 
            return JSON.stringify(this.yourObjectOrArray)
        }
    },
    watch: {
        yourObjectOrArray_str: function(newValue_str, oldValue_str) {
            let newValue = JSON.parse(newValue_str)
            let oldValue = JSON.parse(oldValue_str)
            // some instructions 
        },
    }
}

Here's a modification of @someshinyobject comment (requires lodash):

It watches all properties of the object not just the first level and also finds the changed property for you:

data: {
  model: {
    title: null,
    props: {
      prop1: 1,
      prop2: 2, 
    },
  }
},
watch: {
  computedModel: {
    deep: true,
    handler(newValue, oldValue) => {
      console.log('Change: ', this.getDifference(oldValue, newValue))
    }
  }
},
computed: {
  computedModel() {
     return lodash.cloneDeep(this.model)
   }
},
methods: {
  getDifference() {
     function changes(object, base) {
       return lodash.transform(object, function(result, value, key) {
         if (!lodash.isEqual(value, base[key])) {
           result[key] = (lodash.isObject(value) && lodash.isObject(base[key])) ? changes(value, base[key]) : value
         }
       })
     }
     return changes(object, base)
   }
},

@aidangarza
@darkomenz

If you need to watch the entire model, make the entire model a computed property and just use Object.assign({}, model) like the following:

var vm = new Vue({
    el: "#app",
    data: {
        model: {
            title: null,
            description: null,
            id: null,
            createdDate: null
        },
        message: null
    },
    computed: {
        // watch the entire as a new object
        computedModel: function() {
            return Object.assign({}, this.model);
        }
    },
    watch: {
        computedModel: {
            deep: true,
            handler: function(n, o) {
                var message = "";

                if (n.title !== o.title) {
                    message += "<p>Title updated from \"" + o.title + "\" to \"" + n.title + "\"</p>"
                }
                if (n.description !== o.description) {
                    message += "<p>Description updated from \"" + o.description + "\" to \"" + n.description + "\"</p>"
                }

                this.message = message;
            }
        }
    }
});

Computed properties are simply cached values returned from functions so since we are overwriting the cached object of the computed property with a brand new object _rather than_ mutating a property within the original object, we are able to achieve the results we want. Noted: Object.assign polyfill required for some browsers.

Extended example here: http://jsfiddle.net/1fw0357q/

This is a good solution but if we would like to prevent change, we need to know which field was changed and pass the old param there... If there a way to know which filed was changed?

...
    data() {
      return {
        shop: {
          cost: 0,
          name:  ''
        }
      }
    },

    watch: {
      'shop.cost': function (o,n) {
        this.onChangeCost(o,n)
      }
    },
...

@aidangarza
@darkomenz

If you need to watch the entire model, make the entire model a computed property and just use Object.assign({}, model) like the following:

var vm = new Vue({
    el: "#app",
    data: {
        model: {
            title: null,
            description: null,
            id: null,
            createdDate: null
        },
        message: null
    },
    computed: {
        // watch the entire as a new object
        computedModel: function() {
            return Object.assign({}, this.model);
        }
    },
    watch: {
        computedModel: {
            deep: true,
            handler: function(n, o) {
                var message = "";

                if (n.title !== o.title) {
                    message += "<p>Title updated from \"" + o.title + "\" to \"" + n.title + "\"</p>"
                }
                if (n.description !== o.description) {
                    message += "<p>Description updated from \"" + o.description + "\" to \"" + n.description + "\"</p>"
                }

                this.message = message;
            }
        }
    }
});

Computed properties are simply cached values returned from functions so since we are overwriting the cached object of the computed property with a brand new object _rather than_ mutating a property within the original object, we are able to achieve the results we want. Noted: Object.assign polyfill required for some browsers.

Extended example here: http://jsfiddle.net/1fw0357q/

Just for reference, anybody looking to do this, Object.assign will only prevent top-level change prevention, if you want to handle deeper levels, use JSON.stringify or even JSON.parse( JSON.stringify( value ) ) or something from lodash, etc.

My assumption this would also reflect in Vue computed property, but wanted to mention this as this was a gotcha for me a while back with Object.assign

Was this page helpful?
0 / 5 - 0 ratings