Vue.js computed property not updating
Asked Answered
W

9

86

I'm using a Vue.js computed property but am running into an issue: The computed method IS being called at the correct times, but the value returned by the computed method is being ignored!

My method

computed: {
  filteredClasses() {
    let classes = this.project.classes
    const ret = classes && classes.map(klass => {
      const klassRet = Object.assign({}, klass)
      klassRet.methods = klass.methods.filter(meth => this.isFiltered(meth, klass))
      return klassRet
    })
    console.log(JSON.stringify(ret))
    return ret
  }
}

The values printed out by the console.log statement are correct, but when I use filteredClasses in template, it just uses the first cached value and never updates the template. This is confirmed by Vue chrome devtools (filteredClasses never changes after the initial caching).

Could anyone give me some info as to why this is happening?

Project.vue

<template>
  <div>
    <div class="card light-blue white-text">
      <div class="card-content row">
        <div class="col s4 input-field-white inline">
          <input type="text" v-model="filter.name" id="filter-name" />
          <label for="filter-name">Name</label>
        </div>
        <div class="col s2 input-field-white inline">
          <input type="text" v-model="filter.status" id="filter-status" />
          <label for="filter-status">Status (PASS or FAIL)</label>
        </div>
        <div class="col s2 input-field-white inline">
          <input
            type="text"
            v-model="filter.apkVersion"
            id="filter-apkVersion"
          />
          <label for="filter-apkVersion">APK Version</label>
        </div>
        <div class="col s4 input-field-white inline">
          <input
            type="text"
            v-model="filter.executionStatus"
            id="filter-executionStatus"
          />
          <label for="filter-executionStatus"
            >Execution Status (RUNNING, QUEUED, or IDLE)</label
          >
        </div>
      </div>
    </div>
    <div v-for="(klass, classIndex) in filteredClasses">
      <ClassView :klass-raw="klass" />
    </div>
  </div>
</template>

<script>
import ClassView from './ClassView.vue'

export default {
  name: 'ProjectView',

  props: {
    projectId: {
      type: String,
      default () {
        return this.$route.params.id
      }
    }
  },

  data () {
    return {
      project: {},
      filter: {
        name: '',
        status: '',
        apkVersion: '',
        executionStatus: ''
      }
    }
  },

  async created () {
    // Get initial data
    const res = await this.$lokka.query(`{
            project(id: "${this.projectId}") {
                name
                classes {
                    name
                    methods {
                        id
                        name
                        reports
                        executionStatus
                    }
                }
            }
        }`)

    // Augment this data with latestReport and expanded
    const reportPromises = []
    const reportMeta = []
    for (let i = 0; i < res.project.classes.length; ++i) {
      const klass = res.project.classes[i]
      for (let j = 0; j < klass.methods.length; ++j) {
        res.project.classes[i].methods[j].expanded = false
        const meth = klass.methods[j]
        if (meth.reports && meth.reports.length) {
          reportPromises.push(
            this.$lokka
              .query(
                `{
                           report(id: "${
                             meth.reports[meth.reports.length - 1]
                           }") {
                               id
                               status
                               apkVersion
                               steps {
                                   status platform message time
                               }
                           }
                       }`
              )
              .then(res => res.report)
          )
          reportMeta.push({
            classIndex: i,
            methodIndex: j
          })
        }
      }
    }

    // Send all report requests in parallel
    const reports = await Promise.all(reportPromises)

    for (let i = 0; i < reports.length; ++i) {
      const { classIndex, methodIndex } = reportMeta[i]
      res.project.classes[classIndex].methods[methodIndex].latestReport =
        reports[i]
    }

    this.project = res.project

    // Establish WebSocket connection and set up event handlers
    this.registerExecutorSocket()
  },

  computed: {
    filteredClasses () {
      let classes = this.project.classes
      const ret =
        classes &&
        classes.map(klass => {
          const klassRet = Object.assign({}, klass)
          klassRet.methods = klass.methods.filter(meth =>
            this.isFiltered(meth, klass)
          )
          return klassRet
        })
      console.log(JSON.stringify(ret))
      return ret
    }
  },

  methods: {
    isFiltered (method, klass) {
      const nameFilter = this.testFilter(
        this.filter.name,
        klass.name + '.' + method.name
      )
      const statusFilter = this.testFilter(
        this.filter.status,
        method.latestReport && method.latestReport.status
      )
      const apkVersionFilter = this.testFilter(
        this.filter.apkVersion,
        method.latestReport && method.latestReport.apkVersion
      )
      const executionStatusFilter = this.testFilter(
        this.filter.executionStatus,
        method.executionStatus
      )
      return (
        nameFilter && statusFilter && apkVersionFilter && executionStatusFilter
      )
    },
    testFilter (filter, item) {
      item = item || ''
      let outerRet =
        !filter ||
        // Split on '&' operator
        filter
          .toLowerCase()
          .split('&')
          .map(x => x.trim())
          .map(seg =>
            // Split on '|' operator
            seg
              .split('|')
              .map(x => x.trim())
              .map(segment => {
                let quoted = false,
                  postOp = x => x
                // Check for negation
                if (segment.indexOf('!') === 0) {
                  if (segment.length > 1) {
                    segment = segment.slice(1, segment.length)
                    postOp = x => !x
                  }
                }
                // Check for quoted
                if (segment.indexOf("'") === 0 || segment.indexOf('"') === 0) {
                  if (segment[segment.length - 1] === segment[0]) {
                    segment = segment.slice(1, segment.length - 1)
                    quoted = true
                  }
                }
                if (!quoted || segment !== '') {
                  //console.log(`Item: ${item}, Segment: ${segment}`)
                  //console.log(`Result: ${item.toLowerCase().includes(segment)}`)
                  //console.log(`Result': ${postOp(item.toLowerCase().includes(segment))}`)
                }
                let innerRet =
                  quoted && segment === ''
                    ? postOp(!item)
                    : postOp(item.toLowerCase().includes(segment))

                //console.log(`InnerRet(${filter}, ${item}): ${innerRet}`)

                return innerRet
              })
              .reduce((x, y) => x || y, false)
          )
          .reduce((x, y) => x && y, true)

      //console.log(`OuterRet(${filter}, ${item}): ${outerRet}`)
      return outerRet
    },
    execute (methID, klassI, methI) {
      this.project.classes[klassI].methods[methI].executionStatus = 'QUEUED'
      // Make HTTP request to execute method
      this.$http.post('/api/Method/' + methID + '/Execute').then(
        response => {},
        error => console.log("Couldn't execute Test: " + JSON.stringify(error))
      )
    },
    registerExecutorSocket () {
      const socket = new WebSocket('ws://localhost:4567/api/Executor/')

      socket.onmessage = msg => {
        const { methodID, report, executionStatus } = JSON.parse(msg.data)

        for (let i = 0; i < this.project.classes.length; ++i) {
          const klass = this.project.classes[i]
          for (let j = 0; j < klass.methods.length; ++j) {
            const meth = klass.methods[j]
            if (meth.id === methodID) {
              if (report)
                this.project.classes[i].methods[j].latestReport = report
              if (executionStatus)
                this.project.classes[i].methods[j].executionStatus =
                  executionStatus
              return
            }
          }
        }
      }
    },
    prettyName: function (name) {
      const split = name.split('.')
      return split[split.length - 1]
    }
  },

  components: {
    ClassView: ClassView
  }
}
</script>

<style scoped></style>
Write answered 8/3, 2017 at 18:21 Comment(6)
Just to clarify, the method filteredClasses is being run each time the project.classes data is changing but the return value ret doesn't update?Ledezma
@Ledezma the local variable ret is being modified correctly. Vue just isn't taking that value and updating vm.computed.filteredClassesWrite
Very strange that the console.log(JSON.stringify(ret)) is showing the correct value but return ret is broken. There has to be something else going on, there's no reason there should be a problem. When you say "filteredClasses never changes after the initial caching". What exactly do you mean? In the UI? Lastly, are you certain you don't have a method or data property also called filteredClasses?Ledezma
Here's a fiddle that shows that your general methodology should work: jsfiddle.net/v0673trhLedezma
A codepen or fiddle demonstrating the issue would do wonders here.Geometrize
@BertEvans here is the full source of the offending component pastebin.com/C8Yxbu0f - Working on distilling it into a runnable fiddleWrite
B
100

If your intention is for the computed property to update when project.classes.someSubProperty changes, that sub-property has to exist when the computed property is defined. Vue cannot detect property addition or deletion, only changes to existing properties.

This has bitten me when using a Vuex store with en empty state object. My subsequent changes to the state would not result in computed properties that depend on it being re-evaluated. Adding explicit keys with null values to the Veux state solved that problem.

I'm not sure whether explicit keys are feasible in your case but it might help explain why the computed property goes stale.

Vue reactiviy docs, for more info: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats

Blowfish answered 27/3, 2019 at 14:8 Comment(5)
As the doc linked to above suggests, you can use this.$set(this.someObject, 'b', 2).Guile
@Nick perhaps I'm wrong, but I don't see in which regard this.$set(this.someObject, 'b', 2) might help: if the property 'b' was not initially set, neither a computed nor a watched property help, as they still don't react.Trackandfield
@Trackandfield As stated in the referenced doc “However, it’s possible to add reactive properties to a nested object using the Vue.set(object, propertyName, value) method: Vue.set(vm.someObject, 'b', 2)”. It’s only root level properties which cannot be added.Guile
OMG... Been bitten by this too. I have been struggeling with Vue computeds like using methods and watch logic.. This is such a logical solution, just create the empty prop on Veux. Thanks!Nifty
This has bitten me once.Chlorpromazine
M
36

I've ran into similar issue before and solved it by using a regular method instead of computed property. Just move everything into a method and return your ret. Official docs.

Mottle answered 8/3, 2017 at 19:55 Comment(7)
The link describes the differences between comp. properties and methods. Using methods may fix this issue but it is not the ideal solution since methods do not use caching.Granicus
Thanks ! I've also used this.$forceUpdate().Microbicide
@WilsonFreitas I'd imagine it's the caching causing this issue in the first place, as mentioned in the question.Aureliaaurelian
@AbrahamBrookes I think OP's issue is related to the way he updates the Component state. Vue is unable to detect his changes so the component does not react to them. Caching should not be a problem if the state is updated according to Vue reactivity guidelines.Granicus
@WilsonFreitas After some fiddling with a similar problem, I think you're correct. When OP is using Object.assign() they are adding non-tracked properties to the object. I think I can answer this question.Aureliaaurelian
If you do this too intensively you'll have low framerates very soon. This only circumvents the issue by creating a new one.Ade
Do NOT use this.$forceUpdate. Your problem exists, because you are adding attributes to an object without letting Vue's reactivity system know that it's there. By using this.$set(this.obj, 'property', value), you're effectively setting the value of a property on the object, and letting Vue know that it should watch any updates to the value. You can do this after the component has rendered and still achieve the same functionality, instead of rerendering the entire component, you're only rerendering what's necessary.Reeta
P
14

I had this issue when the value was undefined, then computed cannot detect it changing. I fixed it by giving it an empty initial value.

according to the Vue documentation

enter image description here

Privative answered 27/5, 2020 at 3:31 Comment(1)
This is a correct explanation for a data variable failing to be reactive, but doesn't apply to computed properties, which is what this question is about.Tropism
J
7

NOTE: Instead of this workaround, see my update below.

I have a workaround for this kind of situations I don't know if you like it. I place an integer property under data() (let's call it trigger) and every time the object that I used in computed property changes, it gets incremented by 1. In this way, computed property updates every time the object changes.

Example:

export default {
data() {
  return {
    trigger: 0, // this will increment by 1 every time obj changes
    obj: { x: 1 }, // the object used in computed property
  };
},
computed: {
  objComputed() {
    // do anything with this.trigger. I'll log it to the console, just to be using it
    console.log(this.trigger);

    // thanks to this.trigger being used above, this line will work
    return this.obj.y;
  },
},
methods: {
  updateObj() {
    this.trigger += 1;
    this.obj.y = true;
  },
},
};

UPDATE: Found out a better way in official docs so you don't need something like this.trigger.

Same example as above with this.$set():

export default {
data() {
  return {
    obj: { x: 1 }, // the object used in computed property
  };
},
computed: {
  objComputed() {
    // note that `y` is not a property of `this.obj` initially
    return this.obj.y;
  },
},
methods: {
  updateObj() {
    // now the change will be detected
    this.$set(this.obj, 'y', true);
  },
},
};

here's working a link

Johnette answered 22/9, 2020 at 11:53 Comment(3)
what if the object is changed from outside of your component?Airway
Do you mean changing the object by using ref on the component? Then, you can change trigger's value in the same way you change the object.Johnette
Thanks for this. I was having trouble with a computed property that was depending on an image being loaded to calculate its aspect ratio. Adding a reference to whether or not the image was loaded in my computed method solved the issue. Essentially: if (this.loading) { return -1; } else { ... }Sternpost
B
4

You need to assign a unique key value to the list items in the v-for. Like so..

<ClassView :klass-raw="klass" :key="klass.id"/>

Otherwise, Vue doesn't know which items to udpate. Explanation here https://v2.vuejs.org/v2/guide/list.html#key

Bonnice answered 6/2, 2018 at 22:16 Comment(1)
I've had this solve the issue for me when the property was updated even when the component wasn't being used in an v-for.Devilment
G
3

If you add console.log before returning, you may be able to see computed value in filteredClasses.

But DOM will not updated for some reason.

Then you need to force to re-render DOM.

The best way to re-render is just adding key as computed value like below.

<div
  :key="JSON.stringify(filteredClasses)" 
  v-for="(klass, classIndex) in filteredClasses"
>
  <ClassView
    :key="classIndex"
    :klass-raw="klass"
  />
</div>

Caution:

Don’t use non-primitive values like objects and arrays as keys. Use string or numeric values instead.

That is why I converted array filteredClasses to string. (There can be other array->string convert methods)

And I also want to say that "It is recommended to provide a key attribute with v-for whenever possible".

Grearson answered 31/8, 2019 at 7:47 Comment(0)
R
3

I have the same problem because the object is not reactivity cause I change the array by this way: arrayA[0] = value. The arrayA changed but the computed value that calculate from arrayA not trigger. Instead of assign value to the arrayA[0], you need to use $set for example. You can dive deeper by reading the link below https://v2.vuejs.org/v2/guide/reactivity.html

I also use some trick like adding a cache = false in computed

compouted: {
   data1: {
      get: () => {
         return data.arrayA[0]
      },
      cache: false
   }
}
Rhinitis answered 2/10, 2021 at 9:50 Comment(0)
P
1

For anybody else being stuck with this on Vue3, I just resolved it and was able to get rid of all the this.$forceUpdate()-s that I had needed before by wrapping the values I returned from the setup() function [and needed to be reactive] in a reference using the provided ref() function like this:

import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'CardDisplay',
  props: {
    items: {
      type: Array,
      default: () => []
    },
    itemComponent: Object,
    maxItemWidth: { type: Number, default: 200 },
    itemRatio: { type: Number, default: 1.25 },
    gapSize: { type: Number, default: 50 },
    maxYCount: { type: Number, default: Infinity }
  },
  setup () {
    return {
      containerSize: ref({ width: 0, height: 0 }),
      count: ref({ x: 0, y: 0 }),
      scale: ref(0),
      prevScrollTimestamp: 0,
      scroll: ref(0),
      isTouched: ref(false),
      touchStartX: ref(0),
      touchCurrentX: ref(0)
    }
  },
  computed: {
    touchDeltaX (): number {
      return this.touchCurrentX - this.touchStartX
    }
  },
  ...
}

After doing this every change to a wrapped value is reflected immediately!

Progestin answered 13/2, 2021 at 21:14 Comment(0)
A
0

If you are adding properties to your returned object after vue has registered the object for reactivity then it won't know to listen to those new properties when they change. Here's a similar problem:

let classes = [
    {
        my_prop: 'hello'
    },
    {
        my_prop: 'hello again'
    },
]

If I load up this array into my vue instance, vue will add those properties to its reactivity system and be able to listen to them for changes. However, if I add new properties from within my computed function:

computed: {
    computed_classes: {
        classes.map( entry => entry.new_prop = some_value )
    }
}

Any changes to new_prop won't cause vue to recompute the property, as we never actually added classes.new_prop to vues reactivity system.

To answer your question, you'll need to construct your objects with all reactive properties present before passing them to vue - even if they are simply null. Anyone struggling with vues reactivity system really should read this link: https://v2.vuejs.org/v2/guide/reactivity.html

Aureliaaurelian answered 20/12, 2019 at 4:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.