Subscribe on changes!

watcher for injected value is triggered before destroying the component displayed with `v-if="value"` after the value is set to falsy

avatar
Jul 31st 2022

Vue version

3.2.37 and 2.7.8

Link to minimal reproduction

https://sfc.vuejs.org/#eNqFUstugzAQ/JWVLxAJcNojJZWqfkJ79AXI0hAF2/KDHJD/vWtAKE2k9ObdnR17ZjyxD62L0SMrWWVb02sHFp3X70L2g1bGwQTaqLE/YgYGuwxOEKAzaoCE1pK3Dfflm09FR4nSrYCC3zbjNREPIGSrpCWUUnCIrOlu7sfJelma0DDJIiTO6E3f/YDKuzTdweEdUhoUY33xSAwTNLUpwRmqwi6Dl/1+//+Wl0fseolH2nhdNyq+mEDyqXA46EvtMFausr7J203hmPfdQTAiFAw4ISq+wVnGFlPyodbF2SpJ9k6z8nVgBSth7sQeGRNrwU7OaVtybrs2unW2hTI/nE6F8dKRlALtkDdGXS0aIhYsu+Hg1BzR5AZJmUHzjPMO+sAbaYOQgaTcZ/jsq/TyjK3L4Fq79uGj/I19gS45R+uFnJfWpLagCoo2Axt7U3gS0TTRSgi3KYRfpgb7pw==

Steps to reproduce

  1. provide a ref value with object/undefined
  2. display subcomponent conditionally with (v-if="value")
  3. watch some property of the injected value in the subcomponent
  4. set the ref to an object with the property, then after some time set the ref to undefined

What is expected?

Since the subcomponent is displayed with v-if="value" then when value changes to undefined the subcomponent should be destroyed and the watcher in the subcomponent shouldn't be triggered (and therefore it shouldn't fail)

What is actually happening?

Cannot read properties of undefined (reading 'bar')

System Info

No response

Any additional comments?

Same behaviour can be observed in 2.7 (demo), but not in 2.6 with @vue/composition-api (demo) (that's how I discovered the problem)

avatar
Jul 31st 2022

in case this is not bug , you can use the watch flush:post option

playground - flush:post

avatar
Aug 1st 2022

Thanks for the solution, wouldn't it be a breaking change though? I mean technically it isn't, as prior to 3.x/2.7 there was no ref() and watch(), but what is the reason for being it inconsistent with options API?

Working options API examples: 2.7.8, 3.2.27

And last but not least - is there any chance to make it work same like in options API or 2.6 with plugin? This behaviour makes a migration from 2.6 with plugin to 2.7/3.x harder as I have to go through every single watcher and see if it's not affected

avatar
Aug 1st 2022

The difference between your initial example and the options API one with3.2.27 is not in the watcher behavior. It's in the way you use provide. (However, yes there is a difference between Vue 2 and 3 worth exploring).

Concerning 3.2

In your options API example, doing this will not trigger the watcher because this change is never being propagated to children:

this.container.foo = undefined

reason being: you used the this.container object as the provide options object, and Vue will not update the provided values for any changes you do against that object. So from the perspective of the child, the provided value never changes and that's why the watch doesn't fire.

Change your timeout to the following to see how the change will be watched and console.log'd event though the component will unmount:

  template: '<sub-component v-if="container.foo.bar" />',
  data: () => ({ container: { foo: { bar: true } } }),
  created () {
    setInterval(() => (this.container.foo.bar = false), 1000);
  },

The same codes does not log false in Vue 2, so that's the difference we can explore. The internal effect queue in Vue 3 works a bit differently than Vue 2, and we have had a few edge-casey differences resulting from that. Wether that is worthy of a fix / fixable, I think Evan needs to take a look at that.

The reason for 2.7 composition API being different to Vue 2 Options API but working closer to Vue 3 is just that, I presume - being closer to Vue 3.

And last but not least - is there any chance to make it work same like in options API or 2.6 with plugin? This behavior makes a migration from 2.6 with plugin to 2.7/3.x harder as I have to go through every single watcher and see if it's not affected

With a kind of global setting or something? no.

avatar
Aug 1st 2022

Thanks for the response, I indeed messed provides, here's updated version for 2.7 and 3.2 (and using this.$watch: 2.7 and 3.2).

I used undefined and { bar: true } values for foo as in my original examples, the result is similar to what you've mentioned - 3.2 logs undefined, 2.7 doesn't. Options API version doesn't fail though, as I guess watching foo.bar.baz takes possibility of some variable from the chain being undefined into account, while in Composition API (or in Options API but with this.$watch(() => ...)) you need to take care it yourself.

So I don't really mind Composition API being different from the Options API, the problem is that in

  • 2.6 with Options API
  • 2.6 with Composition
  • 2.7 with Options API

component is destroyed first, and in

  • 2.7 with Composition API
  • 3.2 with Options API
  • 3.2 with Composition API

component is destroyed after watcher is triggered with undefined value. This might be not a big problem in the examples I've provided, worst thing that can happen is just a warning/error in the console, but I'd have to evaluate all of my >250 watchers to make sure that nothing else breaks

With a kind of global setting or something? no.

I meant with some patch

EDIT: if this can't be fixed then I'm pretty sure that at least this change of the behaviour should be added to the migration guide