Vue.js - How to properly watch for nested data

Javascriptvue.jsVue ComponentVue Resource

Javascript Problem Overview


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.

Javascript Solutions


Solution 1 - Javascript

You can use a deep watcher for that:

watch: {
  item: {
     handler(val){
       // do stuff
     },
     deep: true
  }
}

This will now detect any changes to the objects in the item array and additions to the array itself (when used with Vue.set). Here's a JSFiddle: http://jsfiddle.net/je2rw3rs/

EDIT

If you don't want to watch for every change on the top level object, and just want a less awkward syntax for watching nested objects directly, you can simply watch a computed instead:

var vm = new Vue({
  el: '#app',
  computed: {
    foo() {
      return this.item.foo;
    }
  },
  watch: {
    foo() {
      console.log('Foo Changed!');
    }
  },
  data: {
    item: {
      foo: 'foo'
    }
  }
})

Here's the JSFiddle: http://jsfiddle.net/oa07r5fw/

Solution 2 - Javascript

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)

Solution 3 - Javascript

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

Solution 4 - Javascript

> 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

Solution 5 - Javascript

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

  }
}

Solution 6 - Javascript

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 {
		countries : {
			UnitedStates : {
				value: "hello world";
			}.
		},
	};
},
computed : {
	helperName : function(){
		return this.countries.UnitedStates.value;
	},
},
watch : {
	helperName : function(newVal, oldVal){
		// do this...
	}
}

Solution 7 - Javascript

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
   }

   ...
}

Solution 8 - Javascript

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

Solution 9 - Javascript

I've found it works this way too:

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

Solution 10 - Javascript

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.

Solution 11 - Javascript

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

Solution 12 - Javascript

For anyone looking for Vue 3


import { watch } from 'vue';

...
...

watch(
  () => yourNestedObject,              // first param, your object
  (currValue, prevValue) => {          // second param, watcher callback
    console.log(currValue, prevValue);
  },
  { deep: true }                       // third param, for deep checking
);

You can refer to the documentation here: https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watch

Solution 13 - Javascript

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

Solution 14 - Javascript

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPlasticView Question on Stackoverflow
Solution 1 - Javascriptcraig_hView Answer on Stackoverflow
Solution 2 - JavascriptRonCView Answer on Stackoverflow
Solution 3 - JavascriptAlper EbicogluView Answer on Stackoverflow
Solution 4 - JavascriptRolandView Answer on Stackoverflow
Solution 5 - JavascriptLastM4NView Answer on Stackoverflow
Solution 6 - JavascriptZacView Answer on Stackoverflow
Solution 7 - JavascriptgetgladView Answer on Stackoverflow
Solution 8 - JavascriptErik KoopmansView Answer on Stackoverflow
Solution 9 - JavascriptJonathan AriasView Answer on Stackoverflow
Solution 10 - JavascriptkruboView Answer on Stackoverflow
Solution 11 - JavascriptthomasLView Answer on Stackoverflow
Solution 12 - JavascriptRahil WazirView Answer on Stackoverflow
Solution 13 - JavascriptJonathan RuizView Answer on Stackoverflow
Solution 14 - JavascriptTess E. DuursmaView Answer on Stackoverflow