Vue 3 Tutorial: Lesson 5 – Computed Properties and Watchers


Computed Properties
Example 1:
Vue.app

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    
    <p> Number of infants: {{ numberInfants }}</p>
    <p> Number of children: {{ numberChildren }}</p>
    <p> Number of adolescents: {{ numberAdolescents }}</p>
    <p> Number of adults: {{ numberAdults }}</p>
  </div>
</template>

<script lang="ts">
 
var peoples = [
    {'name':'Bob','age':1},
    {'name':'Merry','age':10},
    {'name':'Peter','age':13},
    {'name':'John','age':19},
    {'name':'Jack','age':11},
    {'name':'Stone','age':50},
    {'name':'Jully','age':37},
    {'name':'Jackie','age':5}
]
 
var App = {  
  data() {
        return {peoples}
  },
  computed:{
      numberInfants():number{
          return this["peoples"].filter((a:{age:number})=>a.age<2).length
      },
      numberChildren():number{
          return this["peoples"].filter((a:{age:number})=>(a.age>=2&&a.age<12)).length
      },
      numberAdolescents():number{
          return this["peoples"].filter((a:{age:number})=>(a.age>=12&&a.age<18)).length
      },
      numberAdults():number{
          return this["peoples"].filter((a:{age:number})=>a.age>=18).length
      }    
  }
}
 
export default App
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
#app ul{
  list-style: none;
  padding-inline-start: 0px;
}
</style>

vue example computed properties

Here we have declared a computed property numberInfants.

Try to change the value of books array in the application data and you will see how numberInfants is changing accordingly.

You can data-bind to computed properties in templates just like a normal property. Vue is aware that vm.numberInfants depends on vm.peoples, so it will update any bindings that depend on vm.numberInfants when vm.peoples changes. And the best part is that we’ve created this dependency relationship declaratively: the computed getter function has no side effects, which makes it easier to test and understand.

Computed Caching vs Methods

You may have noticed we can achieve the same result by invoking a method in the expression:

<p> Number of infants: {{ numberOfInfants() }}</p>
  methods: {
    numberOfInfants() {
      return this["peoples"].filter((a:{age:number})=>a.age<2).length
    }
  }

Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed. This means as long as vm.peoples has not changed, multiple access to the numberInfants computed property will immediately return the previously computed result without having to run the function again.

This also means the following computed property will never update, because Date.now() is not a reactive dependency:

computed: {
  now() {
    return Date.now()
  }
}

In comparison, a method invocation will always run the function whenever a re-render happens.

Why do we need caching? Imagine we have an expensive computed property list, which requires looping through a huge array and doing a lot of computations. Then we may have other computed properties that in turn depend on list. Without caching, we would be executing list’s getter many more times than necessary! In cases where you do not want caching, use a method instead.

Computed Setter

Computed properties are by default getter-only, but you can also provide a setter when you need it:

computed: {
  fullName: {
    // getter
    get() {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set(newValue) {
      const names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

Now when you run vm.fullName = 'John Doe', the setter will be invoked and vm.firstName and vm.lastName will be updated accordingly.

Computed properties in class-style component

Method 1: Use Options decorator, for example:

import { Vue, Options } from 'vue-class-component'
@Options({    
  computed: {
    newCount(){
      return this.count+1
    }
  }
})
export default class App extends Vue{

Method 2: Use property accessors, for example:

import { Vue } from 'vue-class-component'
export default class App extends Vue 
{  
  count = 0

  get newCount(){
    return this.count+1
  }

Watchers

While computed properties are more appropriate in most cases, there are times when a custom watcher is necessary. That’s why Vue provides a more generic way to react to data changes through the watch option. This is most useful when you want to perform asynchronous or expensive operations in response to changing data.

Example 2: use axios and watchers:

npm install --save axios

App.vue

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">

    <p>
    Ask a yes/no question:
      <input v-model="question" />
    </p>
    <p>{{ answer }}</p>
  </div>
</template>

<script lang="ts">
 
import axios from 'axios'
 
var App = {
  
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)'
    }
  },
  watch: {
    // whenever question changes, this function will run
    question(newQuestion:string, oldQuestion:string) {
      if (newQuestion.indexOf('?') > -1) {
        this["getAnswer"]()
      }
    }
  },
  methods: {
    getAnswer() {
      this['answer'] = 'Thinking...'
      axios
        .get('https://yesno.wtf/api')
        .then(response => {
          this['answer'] = response.data.answer
        })
        .catch(error => {
          this['answer'] = 'Error! Could not reach the API. ' + error
        })
    }
  }
}

export default App

</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

vue example watch

In this case, using the watch option allows us to perform an asynchronous operation (accessing an API) and sets a condition for performing this operation. None of that would be possible with a computed property.

In addition to the watch option, you can also use the imperative vm.$watch API.

Example 3: use watch handler

{
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)'
    }
  },
  watch: {    
    "question": {
      handler: 'onPropertyChanged',      
      immediate: false,
      deep: true
    }
  },
  methods: {
    onPropertyChanged(newQuestion:string, oldQuestion:string){
      if (newQuestion.indexOf('?') > -1) {
        this["getAnswer"]()
      }
    },
    getAnswer() {
      this['answer'] = 'Thinking...'
      axios
        .get('https://yesno.wtf/api')
        .then(response => {
          this['answer'] = response.data.answer
        })
        .catch(error => {
          this['answer'] = 'Error! Could not reach the API. ' + error
        })
    }
  }
}

Example 4: use class-style component syntax:
App.vue

...

<script lang="ts">
import { Vue, Options } from 'vue-class-component'

import axios from 'axios'
 
@Options({  
  watch: {
    question(newQuestion:string, oldQuestion:string){
      if (newQuestion.indexOf('?') > -1) {
        this.getAnswer()
      }
    }
  }
})
export default class App extends Vue 
{
  question = ''
  answer = 'Questions usually contain a question mark. ;-)'
 
  getAnswer() {
    this.answer = 'Thinking...'
    axios
      .get('https://yesno.wtf/api')
      .then(response => {
        this.answer = response.data.answer
      })
      .catch(error => {
        this.answer = 'Error! Could not reach the API. ' + error
      })
  }
}
 
</script>

...

Computed vs Watched Property

Vue does provide a more generic way to observe and react to data changes on a current active instance: watch properties. When you have some data that needs to change based on some other data, it is tempting to overuse watch – especially if you are coming from an AngularJS background. However, it is often a better idea to use a computed property rather than an imperative watch callback. Consider this example:

Use watchers:

Vue.createApp({
  data() {
    return {
      firstName: 'Foo',
      lastName: 'Bar',
      fullName: 'Foo Bar'
    }
  },
  watch: {
    firstName(val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName(val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
}).mount('#app')

The above code is imperative and repetitive.

Use computed property

Vue.createApp({
  data() {
    return {
      firstName: 'Foo',
      lastName: 'Bar'
    }
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  }
}).mount('#app')

Leave a Reply