Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

newValue and oldValue parameters are the same when deep watching an object #2164

Closed
aidangarza opened this issue Jan 14, 2016 · 36 comments
Closed

Comments

@aidangarza
Copy link

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.

@yyx990803
Copy link
Member

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

@aidangarza
Copy link
Author

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.

@gendalf
Copy link

gendalf commented Nov 10, 2016

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"

@RyanPaiva56
Copy link

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

@uncleGena
Copy link

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;

@BernardMarieOnzo
Copy link

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

@dynalz
Copy link

dynalz commented Mar 30, 2018

@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’t 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)

@BernardMarieOnzo
Copy link

BernardMarieOnzo commented Apr 4, 2018

thank you @dynalz and @uncleGena

@darkomenz
Copy link

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.

@rothariger
Copy link

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

@lukenofurther
Copy link

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?

@FireBrother
Copy link

FireBrother commented Jul 19, 2018

@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) }) }

@someshinyobject
Copy link

someshinyobject commented Oct 24, 2018

@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/

@someshinyobject
Copy link

someshinyobject commented Oct 25, 2018

@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/

@u007
Copy link

u007 commented Mar 25, 2019

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

@lukasjuhas
Copy link

@someshinyobject You are a star, thank you!

@heartz66
Copy link

heartz66 commented Oct 3, 2019

@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.

@julianlecalvez
Copy link

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 
        },
    }
}

@alexhx5
Copy link

alexhx5 commented Jan 12, 2020

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)
   }
},

@jack-fdrv
Copy link

@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?

@craftsman-expert
Copy link

craftsman-expert commented Jun 4, 2020


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

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

@tripflex
Copy link

@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

@gabrielpuzl
Copy link

gabrielpuzl commented Apr 28, 2021

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 
        },
    }
}

oh god

you saved my life

:)) good!!

@johnyK
Copy link

johnyK commented Jun 19, 2021

Nice workaround!!

@mashirozx
Copy link

Still meet this issue on Vue3...

@verybigelephants
Copy link

verybigelephants commented Jul 30, 2021

the trick from @julianlecalvez is awesome! however, if you need to performance from the stringifying and your values change often (like values changing each few miliseconds from some input) you could do this the hard way

data(){
    complicated_dynamic_object = { ... }
    ...

    watched: complicated_dynamic_object,
    watched_old: JSON.parse(JSON.stringify(complicated_dynamic_object)),
},

watch:{
    watched: { 
        handler(oldval, newval){
           var changed_paths = {};

            for(property in newval){
                 ... a lot of nested loops here, traversing everything i need ...
 
                if(newval[property].alot[of].nested[paths] != this.watched_old[property].alot[of].nested[paths]){
                    changed_paths = { path1: property, path2: of, path3:paths };
                    this.watched_old[property].alot[of].nested[paths] = newval[property].alot[of].nested[paths];
                }
            }

            ... your logic for a changed value in changed_paths ...
        },
        deep:true
    }
}

@anzuj
Copy link

anzuj commented Jan 28, 2022

@julianlecalvez you're a star and saved my day today, even 3 years later! 👍

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 
        },
    }
}

@SgtChrome
Copy link

SgtChrome commented Mar 17, 2022

Hey guys, how come @julianlecalvez' solution works in the options API but not in the composition API?

// OptionsAPI works
  computed: {
    category() {
      return this.$store.getters.activeCategory
    },
    categoryString() {
      return JSON.stringify(this.category)
    }
  },
  watch: {
    categoryString: function(n, o) {
      console.log('ho', n, o)
    }
  }
// Composition api doesn't
  setup() {
    const store = useStore()
    const category = computed(() => store.getters.activeCategory)
    const categoryString = computed(() => JSON.stringify(category.value))
    watch(
      () => categoryString,
      (o,n) => {
        console.log('test', o.value,n.value)
        startUpdateTimer()
      }, {deep:true}
    )
  }

Edit: Please excuse me, the deep: true in the watcher needs to go.

@Wiejeben
Copy link

Wiejeben commented Jan 5, 2023

Instead of using watchers I was able to get a bit more information out of objects using the build in JavaScript Proxy object:

data() {
    return {
        form: new Proxy({
            salutation: '',
            firstName: '',
            suffix: '',
            lastName: '',
        }, {
            set(target, key, value) {
                console.log(`${key} set to ${value}`);
                target[key] = value;
                return true;
            }
        })
    }
}

@akshaysalunke13
Copy link

akshaysalunke13 commented Feb 1, 2023

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

@yyx990803 Where can we find this note now that the docs have been updated?

Edit: Never mind found it. For any one that stumbles upon this, here it is: https://v2.vuejs.org/v2/api/#vm-watch

@velly
Copy link

velly commented Jun 26, 2023

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

@yyx990803 Where can we find this note now that the docs have been updated?

Edit: Never mind found it. For any one that stumbles upon this, here it is: https://v2.vuejs.org/v2/api/#vm-watch

Hey, akshaysalunke13,

The note is in the sample code. E.G.,
watch(
() => state.someObject,
(newValue, oldValue) => {
// Note: newValue will be equal to oldValue here
// unless state.someObject has been replaced
},
{ deep: true }
)

@evankford
Copy link

evankford commented Jul 10, 2023

For those of you wanting a quick and dirty Composition API solution, this works for me.

<script setup>
import {computed, ref, watch} from 'vue'
const stateObject = ref({ key1: value1, key2: []})
const copiedStateObject = computed(() => Object.assign({}, stateObject.value)) // Creates a new object on each change
// Wrong way, cannot compare changes in the same JS object
watch(stateObject, (value, oldValue) => {
    console.log(value, oldValue) // Will always be the same, even with deep === true
})
// Right way, comparing two different objects
watch(copiedStateObject, (value, oldValue) => {
    console.log(value, oldValue) // Correctly reflects changes
}, {
    deep: true // This is odd, but it seems like watching updates in internal arrays still require deep: true, even with the new assignment
}) 
</script>

Obviously, the concern here is runaway memory consumption by creating a new JS object every time there's a change in the form. From using the inspector, I think that computed and Object.assign() are correctly garbage-collecting and there's not a runaway issue. That said, a more memory-safe solution, while a bit less concise, would be:

<script setup>
import {computed, ref, watch} from 'vue'
const stateObject = ref({ key1: value1, key2: []})
const stateObjectLast = computed({})
// Can't use oldValue, instead use manually set stateObjectLast
watch(stateObject, (value) => {
    console.log(value, stateObjectLast.value) // Correctly reflects changes
    // Side effects/validation here
     
    stateObjectLast.value = value // Store current state to compare changes on next watch event.
}, 
{ deep: true} // Necessary to catch inner mutations to stateObject.
) 
</script>

It feels like this could be something supported in the library in the style of {deep: true}. Let me know if this is something worth contributing, or if it seems like a slippery slope/may cause unnecessary confusion.

@smohammadhn
Copy link

Seems that there is no nice way of doing it, whether you should give up or use one of the workarounds other guys suggested.

from the 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’t keep a copy of the pre-mutate value.

@MickL
Copy link

MickL commented Feb 24, 2024

For watching individual changes on large objects (like forms), isnt this a solution?

<script setup lang="ts">
const state = reactive({
  firstName: '',
  lastNamename: '',
  street: '',
  city: '',
});

Object.keys(state).forEach((key) => {
  watch(() => state[key], async (newState, oldState) => {
     console.log(`'${key}' has changed from '${oldState}' to '${newState}'`);
  });
});
</script>

@suterma
Copy link

suterma commented Apr 5, 2024

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

Today, it's in https://vuejs.org/api/reactivity-core.html#watch, near the end, saying

...the new value and the old will be the same object...

@bennyschudel
Copy link

bennyschudel commented May 23, 2024

There is a simple way to achieve this with arrays or objects:

watch(() => [...arrayToWatch.value], (newValue, oldValue) => {
  ...
});

For objects you could use:

watch(() => ({...objectToWatch.value}), (newValue, oldValue) => {
  ...
});

Be aware that the value inside the collection could be still a reference. If you don't want that, you would need a cloning method on top.

watch(() => structuredClone({...objectToWatch.value}), (newValue, oldValue) => {
  ...
});

Am I missing something?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests