Vue: Interesting bug / quirk computed property not updating correctly, but probably not fixable

Created on 7 Apr 2020  路  15Comments  路  Source: vuejs/vue

Hi consider this snippet:

  ...
  computed: {
    someProp() {
      if(this.nonReactiveProperty) {
        return this.reactiveProperty;
      }
   }
  },
  ...

From the documentation I would assume (please show me if I missed something) that someProp() should get re-evaluated when this.reactiveProperty does change. Works as expected as long as this.nonReactiveProprty is true all time. However, if this.nonReactiveProperty is false the first time someProp() is read, then someProp() will never re-evaluate, even if this.reactiveProperty changes (after this.nonReactiveProperty gets true).
The problem is, I guess it is not fixable. Am I assuming correctly, that the dependencies of someProp() get calculated the first time it is executed? So code which does not get executed then, vue is just blind of....?
This is quite tricky and probably I've run into this quite some times before figuring out. It gets even harder to see when inside lazy conditions. E.g. this works all the time:

someProp() {
   if(this.reactiveProperty && this.nonReactiveProperty) {
     return this.reactiveProperty;
   }
}

while this might not:

someProp() {
   if(this.nonReactiveProperty && this.reactiveProperty) {
     return this.reactiveProperty;
   }
}

So if this is not fixable, and I didn't miss something, I would suggest to add this to the documentation.

EDIT: here is a codesandbox: https://codesandbox.io/s/priceless-leftpad-kktuq?file=/src/App.vue

best
Martin

Most helpful comment

@derMart

EDIT: Nvm, noticed you mentioned that in the last reply. 馃槄 I think that behaviour could be better described in the API docs.

It鈥檚 worth adding that (as far as I remember) the dependencies are collected whenever the computed property executes without using the cache. Any dependencies collected during previous runs will also be removed.

So in case you have several conditionals:

someProp () {
  if (this.reactiveProp === 0) {
    return this.anotherReactiveProp
  } else if (this.yetAnotherReactiveProp === 1) {
    return this.nonReactiveProp
  } else {
    return this.alsoReactiveProp
  }
}

When this.reactiveProp === 0 is true, the only dependencies collected will be: this.reactiveProp and this.anotherReactiveProp.
When this.reactiveProp === 0 is false and this.yetAnotherReactiveProp === 1 is true, the collected dependencies list will be: this.reactiveProp and this.yetAnotherReactiveProp.
When neither of those two conditions pass (final else returns), the list of dependencies will look like this: this.reactiveProp, this.yetAnotherReactiveProp and this.alsoReactiveProp.

As you can see the list of dependencies is dynamic and collected at runtime as it was already mentioned. Which means if during the last execution of that computed property something wasn鈥檛 registered as a dependency, changing it won鈥檛 trigger a recalculation. And this is ultimately a good thing if you think about it.

All 15 comments

Hello, thank you for taking time filling this issue!

However, we kindly ask you to use our Issue Helper when creating new issues, in order to ensure every issue provides the necessary information for us to investigate. This explains why your issue has been automatically closed by me (your robot friend!).

I hope to see your helper-created issue very soon!

oh comeon guys, this is ridiculous and not helpful!

@derMart The documentation repository is at vuejs/vuejs.org, please check existent issues there first as this has probably been brought up already.

write -g KeyRepeat ?

well if you mean https://github.com/vuejs/vuejs.org I did search there already, and I have not found anything!

Not a bug: https://vuejs.org/v2/api/#computed Please look at the last sentence under "Details"

Computed properties are cached, and only re-computed on reactive dependency changes. Note that if a certain dependency is out of the instance鈥檚 scope (i.e. not reactive), the computed property will not be updated.

thank you for the answer @sirlancelot but IMHO this sentence does not apply to the issue.
The sentence would apply to a situation like this:

  computed: {
    someProp() {
      return this.nonReactiveProperty;
    }
  }

But the dependency here in question (this.reactiveProperty, see above) is a reactive property, so according to this sentence, it should be inside the instance's scope (as it is reactive).

could you please reopen the issue until it is resolved, thank you very much!

There's nothing to resolve here, maybe documentation needs some clarification but it should be reported in the docs repo @derMart

ok, so to clarify (as it has not been done) the behaviour is working as intended?

Yes, because when the computed is computed for the first time the reactive property is not accessed so it is not added to dependencies
Not at computer now but probably adding this.reactiveProperty; before the if would make it work as you expect

This is how JavaScript works. Your code goes through the JavaScript runtime. Vue tracks dependencies at runtime. What this means is at runtime if your computed property doesn't access something that is reactive, then it has zero dependencies. If you want to discuss this behavior, please move to a discussion platform such as Discord or Forums

No, the purpose of this issue was not to discuss how vue.js should handle dependency calculation, and frankly I don't think I have written anything in this direction. The purpose is to understand it and now that some of my initial assumptions were confirmed, to kindly provoke documentation updates to improve this lovely library even further and make it easier for others to use (reading this issue might contribute to that too).

However, as you brought it up, I have to respond to one thing I strongly disagree on.
This issue has nothing to do with "how Javascript works", but all to do about how the vue creators decided to handle dependency detection (which as I said do not intend to judge). There are other options to track dependencies than executing the code and track getters. E.g. static code analysis would come to my mind here (functions have a .toString() method showing their code) and probably other options I do not know of. Of course, those other options have different trade-offs (for code analysis e.g. hard to capture property access in cases where property keys are dynamically calculated, while at the same time the issue stated would be no problem). Discussing those indeed might be out of the scope of this issue, however I personally wouldn't dare to ask anybody else to discuss such topics outside of github. In any case, as Javascript leaves multiple options, it is not obvious how vue handles dependency tracking.

Here is an example codesandbox which tries to reconstruct dependency detection in more detail:
https://codesandbox.io/s/nifty-darkness-fqf6x?file=/src/App.vue
See the console output and comments.
My conclusion of this is, that dependencies are re-tracked every time a re-evaluation of a computed property is triggered. This also means that reactive properties which once were identified dependencies might be removed from the list after a later evaluation during which they were not accessed.

Another conclusion is the workaround suggested by @jacekkarczmarczyk (with a little loss in execution performance): Just put all the needed reactive dependencies as pure statements at the beginning of the computed property, in case of the example above:

computed: {
  someProp() {
    this.reactiveProperty;
    if(this.nonReactiveProperty) {
      return this.reactiveProperty;
    }
  }
}

This will call their getter and mark them as dependent.

@derMart

EDIT: Nvm, noticed you mentioned that in the last reply. 馃槄 I think that behaviour could be better described in the API docs.

It鈥檚 worth adding that (as far as I remember) the dependencies are collected whenever the computed property executes without using the cache. Any dependencies collected during previous runs will also be removed.

So in case you have several conditionals:

someProp () {
  if (this.reactiveProp === 0) {
    return this.anotherReactiveProp
  } else if (this.yetAnotherReactiveProp === 1) {
    return this.nonReactiveProp
  } else {
    return this.alsoReactiveProp
  }
}

When this.reactiveProp === 0 is true, the only dependencies collected will be: this.reactiveProp and this.anotherReactiveProp.
When this.reactiveProp === 0 is false and this.yetAnotherReactiveProp === 1 is true, the collected dependencies list will be: this.reactiveProp and this.yetAnotherReactiveProp.
When neither of those two conditions pass (final else returns), the list of dependencies will look like this: this.reactiveProp, this.yetAnotherReactiveProp and this.alsoReactiveProp.

As you can see the list of dependencies is dynamic and collected at runtime as it was already mentioned. Which means if during the last execution of that computed property something wasn鈥檛 registered as a dependency, changing it won鈥檛 trigger a recalculation. And this is ultimately a good thing if you think about it.

Was this page helpful?
0 / 5 - 0 ratings