[javascript] Vue.js - How to properly watch for nested data

I'm trying to understand how to properly watch for some prop variation. I have a parent component (.vue files) that receive data from an ajax call, put the data inside an object and use it to render some child component through a v-for directive, below a simplification of my implementation:

<template>
    <div>
        <player v-for="(item, key, index) in players"
            :item="item"
            :index="index"
            :key="key"">
        </player>
    </div>
</template>

... then inside <script> tag:

 data(){
     return {
         players: {}
 },
 created(){
        let self = this;
        this.$http.get('../serv/config/player.php').then((response) => {
            let pls = response.body;
            for (let p in pls) {
                self.$set(self.players, p, pls[p]);
            }
    });
}

item objects are like this:

item:{
   prop: value,
   someOtherProp: {
       nestedProp: nestedValue,
       myArray: [{type: "a", num: 1},{type: "b" num: 6} ...]
    },
}

Now, inside my child "player" component I'm trying to watch for any Item's property variation and I use:

...
watch:{
    'item.someOtherProp'(newVal){
        //to work with changes in "myArray"
    },
    'item.prop'(newVal){
        //to work with changes in prop
    }
}

It works but it seems a bit tricky to me and I was wondering if this is the right way to do it. My goal is to perform some action every time prop changes or myArray gets new elements or some variation inside existing ones. Any suggestion will be appreciated.

This question is related to javascript vue.js vue-component vue-resource

The answer is


Another way to add that I used to 'hack' this solution was to do this: I set up a seperate computed value that would simply return the nested object value.

data : function(){
    return {
        my_object : {
            my_deep_object : {
                my_value : "hello world";
            }.
        },
    };
},
computed : {
    helper_name : function(){
        return this.my_object.my_deep_object.my_value;
    },
},
watch : {
    helper_name : function(newVal, oldVal){
        // do this...
    }
}

I used deep:true, but found the old and new value in the watched function was the same always. As an alternative to previous solutions I tried this, which will check any change in the whole object by transforming it to a string:

created() {
    this.$watch(
        () => JSON.stringify(this.object),
            (newValue, oldValue) => {
                //do your stuff                
            }
    );
},

Here's a way to write watchers for nested properties:

    new Vue({
        ...allYourOtherStuff,
        watch: {
            ['foo.bar'](newValue, oldValue) {
                // Do stuff here
            }
        }
    });

You can even use this syntax for asynchronous watchers:

    new Vue({
        ...allYourOtherStuff,
        watch: {
            async ['foo.bar'](newValue, oldValue) {
                // Do stuff here
            }
        }
    });


I've found it works this way too:

watch: {
    "details.position"(newValue, oldValue) {
        console.log("changes here")
    }
},
data() {
    return {
      details: {
          position: ""
      }
    }
}

How if you want to watch a property for a while and then to un-watch it?

Or to watch a library child component property?

You can use the "dynamic watcher":

this.$watch(
 'object.property', //what you want to watch
 (newVal, oldVal) => {
    //execute your code here
 }
)

The $watch returns an unwatch function which will stop watching if it is called.

var unwatch = vm.$watch('a', cb)
// later, teardown the watcher
unwatch()

Also you can use the deep option:

this.$watch(
'someObject', () => {
    //execute your code here
},
{ deep: true }
)

Please make sure to take a look to docs


Tracking individual changed items in a list

If you want to watch all items in a list and know which item in the list changed, you can set up custom watchers on every item separately, like so:

var vm = new Vue({
  data: {
    list: [
      {name: 'obj1 to watch'},
      {name: 'obj2 to watch'},
    ],
  },
  methods: {
    handleChange (newVal, oldVal) {
      // Handle changes here!
      // NOTE: For mutated objects, newVal and oldVal will be identical.
      console.log(newVal);
    },
  },
  created () {
    this.list.forEach((val) => {
      this.$watch(() => val, this.handleChange, {deep: true});
    });
  },
});

If your list isn't populated straight away (like in the original question), you can move the logic out of created to wherever needed, e.g. inside the .then() block.

Watching a changing list

If your list itself updates to have new or removed items, I've developed a useful pattern that "shallow" watches the list itself, and dynamically watches/unwatches items as the list changes:

// NOTE: This example uses Lodash (_.differenceBy and _.pull) to compare lists
//       and remove list items. The same result could be achieved with lots of
//       list.indexOf(...) if you need to avoid external libraries.

var vm = new Vue({
  data: {
    list: [
      {name: 'obj1 to watch'},
      {name: 'obj2 to watch'},
    ],
    watchTracker: [],
  },
  methods: {
    handleChange (newVal, oldVal) {
      // Handle changes here!
      console.log(newVal);
    },
    updateWatchers () {
      // Helper function for comparing list items to the "watchTracker".
      const getItem = (val) => val.item || val;

      // Items that aren't already watched: watch and add to watched list.
      _.differenceBy(this.list, this.watchTracker, getItem).forEach((item) => {
        const unwatch = this.$watch(() => item, this.handleChange, {deep: true});
        this.watchTracker.push({ item: item, unwatch: unwatch });
        // Uncomment below if adding a new item to the list should count as a "change".
        // this.handleChange(item);
      });

      // Items that no longer exist: unwatch and remove from the watched list.
      _.differenceBy(this.watchTracker, this.list, getItem).forEach((watchObj) => {
        watchObj.unwatch();
        _.pull(this.watchTracker, watchObj);
        // Optionally add any further cleanup in here for when items are removed.
      });
    },
  },
  watch: {
    list () {
      return this.updateWatchers();
    },
  },
  created () {
    return this.updateWatchers();
  },
});

Personally I prefer this clean implementation:

watch: {
  myVariable: {
     handler(newVal, oldVal){  // here having access to the new and old value
       // do stuff
     },
     deep: true,
     immediate: true //  Also very important the immediate in case you need it, the callback will be called immediately after the start of the observation

  }
}

VueJs deep watch in child objects

new Vue({
    el: "#myElement",
    data: {
        entity: {
            properties: []
        }
    },
    watch: {
        'entity.properties': {
            handler: function (after, before) {
                // Changes detected. Do work...     
            },
            deep: true
        }
    }
});

Another good approach and one that is a bit more elegant is as follows:

 watch:{
     'item.someOtherProp': function (newVal, oldVal){
         //to work with changes in someOtherProp
     },
     'item.prop': function(newVal, oldVal){
         //to work with changes in prop
     }
 }

(I learned this approach from @peerbolte in the comment here)


None of the answer for me was working. Actually if you want to watch on nested data with Components being called multiple times. So they are called with different props to identify them. For example <MyComponent chart="chart1"/> <MyComponent chart="chart2"/> My workaround is to create an addionnal vuex state variable, that I manually update to point to the property that was last updated.

Here is a Vuex.ts implementation example:

export default new Vuex.Store({
    state: {
        hovEpacTduList: {},  // a json of arrays to be shared by different components, 
                             // for example  hovEpacTduList["chart1"]=[2,6,9]
        hovEpacTduListChangeForChart: "chart1"  // to watch for latest update, 
                                                // here to access "chart1" update 
   },
   mutations: {
        setHovEpacTduList: (state, payload) => {
            state.hovEpacTduListChangeForChart = payload.chart // we will watch hovEpacTduListChangeForChart
            state.hovEpacTduList[payload.chart] = payload.list // instead of hovEpacTduList, which vuex cannot watch
        },
}

On any Component function to update the store:

    const payload = {chart:"chart1", list: [4,6,3]}
    this.$store.commit('setHovEpacTduList', payload);

Now on any Component to get the update:

    computed: {
        hovEpacTduListChangeForChart() {
            return this.$store.state.hovEpacTduListChangeForChart;
        }
    },
    watch: {
        hovEpacTduListChangeForChart(chart) {
            if (chart === this.chart)  // the component was created with chart as a prop <MyComponent chart="chart1"/> 
                console.log("Update! for", chart, this.$store.state.hovEpacTduList[chart]);
        },
    },

My problem with the accepted answer of using deep: true, is that when deep-watching an array, I can't easily identify which element of the array contains the change. The only clear solution I've found is this answer, which explains how to make a component so you can watch each array element individually.


Not seeing it mentioned here, but also possible to use the vue-property-decorator pattern if you are extending your Vue class.

import { Watch, Vue } from 'vue-property-decorator';

export default class SomeClass extends Vue {
   ...

   @Watch('item.someOtherProp')
   someOtherPropChange(newVal, oldVal) {
      // do something
   }

   ...
}

Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to vue.js

How to fix 'Unchecked runtime.lastError: The message port closed before a response was received' chrome issue? Center content vertically on Vuetify Vue.js get selected option on @change Using Environment Variables with Vue.js did you register the component correctly? For recursive components, make sure to provide the "name" option Vue 'export default' vs 'new Vue' How can I go back/route-back on vue-router? Change the default base url for axios How to reference static assets within vue javascript How to change port number in vue-cli project

Examples related to vue-component

Vue 'export default' vs 'new Vue' Vuex - Computed property "name" was assigned to but it has no setter How to add external JS scripts to VueJS Components How to listen for 'props' changes How can I set selected option selected in vue.js 2? How do I format currencies in a Vue component? [Vue warn]: Property or method is not defined on the instance but referenced during render VueJs get url query Vue.js - How to properly watch for nested data Vue template or render function not defined yet I am using neither?

Examples related to vue-resource

Vue.js - How to properly watch for nested data