Subscribe on changes!

Expressions referencing undeclared properties do not update reactively

avatar
Jan 3rd 2023

Vue version

3.2.45

Link to minimal reproduction

https://codepen.io/leaverou/pen/poZEvpb

Steps to reproduce

Two flows:

  • Flow A
    1. Edit number in second input (labelled "Undeclared"). Nothing happens.
    2. Edit number in first input (labelled "Declared"). Both now update.
  • Flow B
    1. Click button labelled "bar = 42". Nothing happens.
    2. Edit number in first input (labelled "Declared"). Both now update.

What is expected?

Second input should update bar as edited. Button should update bar when clicked. These updates to bar should be reflected in the {{ bar }} expression immediately.

What is actually happening?

See above.

System Info

No response

Any additional comments?

Oddly, app.bar reflects the current value of the input, so it seems to be a bug with updating the expressions.

avatar
Jan 3rd 2023

I'm not sure what you are expecting. Only reactive values trigger re-renders. That's why we have reactivity in Vue. baris not reactive. So nothing updates when it's being changed.

All of that is expected

avatar
Jan 3rd 2023

I was under the impression that with Vue3 using proxies, adding reactive properties that are not in the initial data would be possible? If neither v-model nor directly adding them on instance does that, what does?

avatar
Jan 3rd 2023

Oh, now i understand the cause of the misconception.

What you refer to applies to adding new property to a already reactive object that you declared in data. That did not work without a helper method in Vue 2, and now works natively in Vue 3.

What you did was adding a new property to the component instance itself, which is not the same thing.

Those were and still are are non-reactive.

avatar
Jan 3rd 2023
avatar
Jan 4th 2023

I see, thank you for explaining, and even more for doing so with a testcase! It seems like a usability bug if the root data object behaves differently than child objects in that way. Given that the root data object is also a proxy, it seems like it would be possible to implement this behavior for that too. What is the rationale for only implementing this behavior for nested objects? TIA!

avatar
Jan 4th 2023

I get your point of view. Several things to consider:

First off, technically, the root object is special, it represents the component instance itself. while it's a proxy, it's not a reactive data proxy - instead the proxy's job is to provide the template access to the different properties defined in the options:

  • properties from the data
  • computed properties from computed(who you can't reassign)
  • functions from the methods options
  • component APIs such as $emit, $el or even the original plain, nonreactive options object with $options etc.

That would not hinder us, in theory, to turn undeclared properties into reactive ones. However, undeclared properties are (at least in Options API) the only ergonomic way to have a property where to store *non-reactive data, where changing it, or reassigning the property, will not trigger the render effect.

While not a day-to-day thing one needs, it's quite common one needs to save something on the instance in a non-reactive way.

export default {
  data: () => ({
     foo: "I'm reactive"
  }),
  created() {
    this.bar = "I'm non reactive"
  },
}

Having said that, I get how it can be confusing. The way "this" behaves in JS is confusing enough, and in Vue Options API, "this" is even more strange.

That's in fact one of the reasons we had Composition API. When using the modern <script setup> syntax, that should should be much more straightforward (but requires a build setup):

<script setup>
import { ref, reactive } from 'vue'

// nonreactive
let  bar = ''

con state = reactive({
  a: 'A',
 b: 'B'
})

// let reassignments are unobservable, so to reactiveley reassign a value, use a ref()
const reassignableBaz = ref('Bazzom')


// Whatever is defined at the top-level here is accessible from within the template, much like a function closure would work. 
// Even imports. Properties can't be created from "nothing" though.
// So you are forced to be more explicit, but you don't have to think about the different aspects of Vue's root instance (`this`)
</script>
<template>
<button @click="bar = 'bar'">{{ bar }}</button><!-- does not update anything, nonreactive -->
<button @click="state.A = 'C'">{{ state.A }}</button>
<button @click="reassignableBaz = 'baz'">{{ reassignableBaz }}</button>

</template>
avatar
Jan 14th 2023

Thank you for the lengthy explanation, I now understand the rationale much better.

First off, technically, the root object is special, it represents the component instance itself. while it's a proxy, it's not a reactive data proxy - instead the proxy's job is to provide the template access to the different properties defined in the options:

  • properties from the data
  • computed properties from computed(who you can't reassign)
  • functions from the methods options
  • component APIs such as $emit, $el or even the original plain, nonreactive options object with $options etc.

If the issue is naming clashes, doesn't that also exist with the data returned from data()? If the issue is code separation, I’d argue that this is an implementation detail that should not affect the public-facing API, which should be designed to be maximally intuitive.

That would not hinder us, in theory, to turn undeclared properties into reactive ones. However, undeclared properties are (at least in Options API) the only ergonomic way to have a property where to store *non-reactive data, where changing it, or reassigning the property, will not trigger the render effect.

I'd actually expect both this.foo and this.bar in your example to be reactive. You’re right there are use cases for non-reactive data, and that they should be possible, but since these are more rare and very conscious, it doesn't have to be as easy as just adding properties, it could require a separate helper or other opt-in step. I — and my hypothesis is most Vue users — see reactivity as the default in Vue, so it makes sense for non-reactivity to require an explicit opt-in.

To test this hypothesis, I started a carefully worded quiz/poll on both Twitter and Mastodon. Preliminary results confirm that the current behavior is indeed not intuitive: out of the people who haven't tried this before, ~3 out of 4 on Twitter and 6 out of 7 on Mastodon (at the time of posting this) expected b to be reactive! These results are very preliminary, there are only 113 votes on Twitter and 32 on Mastodon right now, so this could change, though it's such a landslide at this point that I'd be very surprised if it changes so much that it favors the current behavior.

avatar
Jan 30th 2023

@LinusBorg

Just figured I'd post an update, since both polls have now closed.

Twitter poll: image

Note that the ratio of people who expect the property to be reactive vs people who expect the current behavior is 2.5:1. The sample size was 617 people.

Mastodon poll: image

Here, that the ratio of people who expect the property to be reactive vs people who expect the current behavior is 3.5:1! (sample size = 175 people)

From a usability standpoint, I'd say that's a pretty clear signal that defaulting to making new properties reactive would make Vue more learnable, and less surprising (I understand there might be performance considerations against it — though I'm happy to help brainstorm implementation if there's general consensus it would be a good idea).

Anyhow, hope this might be useful in some way, in case you're ever debating changing that design decision. 🤷🏽‍♀️ Meanwhile, is there any way to opt in to the other behavior and add a property to the root that is actually reactive?