Adding to this.$options.computed during `beforeCreate` only works if `computed` is already defined on component
Version
3.0.4
Reproduction link
https://jsfiddle.net/6k3nod2c/
Steps to reproduce
Create a component / app with a mixin that adds to this.$options.computed
.
const mixin = {
beforeCreate() {
if (!this.$options.computed) {
this.$options.computed = {};
}
this.$options.computed.value = () => {
return 'works';
}
}
}
Vue.createApp({
mixins: [mixin],
// computed: {} // Uncommenting this line makes it work
}).mount('#app')
What is expected?
Expect this.value
to be "works"
.
What is actually happening?
this.value
is undefined
.
In vue 2 this worked.
In vue 3 this only works if the computed
option is already defined.
only components
change could be expected work right in beforeCreated
, not including dynamic computed
props
inject
.
because they are pre defined in the prototype, not define for each instance. and only root instance (new Vue
/ Vue.createApp
) won't do that, component in compontents
or registered in Vue.component
or extended by Vue.extend
, they will all do that for performance.
why not do below simply in your sample?:
const mixin = {
computed: {
value: () => 'works'
}
}
The example was a simplification, so it's not that simple. I encountered this bug when upgrading from Vue 2 where this used to work.
In our app we have a global mixin that looks at another $options to generate computed properties.
Something like:
const mixin = {
beforeCreate() {
const magicOptions = this.$options.magic;
for (const alias in magicOptions) {
// subscribe to kv store
const getterAndSetter = subscribe(magicOptions[alias]);
this.$options.computed[alias] = getterAndSetter;
}
},
unmounted() {
// unsubscribe when
this.$options.magic.forEach(unsubscribe);
}
}
Then any component can define a "magic" object to subscribe to the backend, and subscription is stopped when component is removed.
export default {
magic: {
state: 'some-key-in-the-kv-store',
},
mounted() {
console.log('state', this.state);
},
methods: {
start() {
this.state = 'start';
}
}
}
I might be doing this the wrong way, so any suggestions are welcome.
The example was a simplification, so it's not that simple.
I see. I think add a hook beforeExtend
which run once for each component would really help. There has been many thing indirect and inefficient for plugins via beforeCreate
.
I see. I think add a hook beforeExtend which run once for each component would really help. There has been many thing indirect and inefficient for plugins via beforeCreate.
Add a lifecycle hook beforeExtend
seems like not a recommendable resolve:
- User have to modify code because its inconsistent with vue2
- Ambiguous because its overlap with
beforeCreate
- Efficiency has not change because extends and mixins can also hold this hook(In fact i don't understand why
beforeCreate
is indirect and inefficient)
We ran into the same issue in our app where we're creating a validation helper similar to Vuelidate.
We define a validations
option in a component when we want some data properties to be validated, and a global mixin will add this to a computed
property (so that validation rules can rely on other data properties with reactivity).
We ran into two problems:
- As mentioned in this issue, if we didn't add an empty
computed
definition to our mixin, adding a dynamic computed property inbeforeCreate
didn't work. This seems due to howoptions
is destructured at the start ofapplyOptions
: https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/componentOptions.ts#L478-L509
I believe this could be fixed by only defining computedOptions
immediately before it's used: https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/componentOptions.ts#L647
const computedOptions = options.computed;
if (computedOptions) {
...
This way if a beforeCreate
hook added a computed
object to options
dynamically, it should be accessible here.
- When we tried adding a
computed
definition to the global mixin, thecomputed
object reference was shared between all component instances
For example, adding to the example from @MiniGod:
const mixin = {
computed: {},
beforeCreate() {
if (condition) {
this.$options.computed.example = () => {
return 'works';
}
}
}
}
In theory this fixes the issue, however since mixin
is defined as a const in our module, the reference to the mixin.computed
object is shared among all component instances that apply the mixin. Then when the condition passes for any instance, and example
is added to this.$options.computed
, it is added to the referenced object, and hence it gets added to every instance created afterwards.
This may be something to be added in a separate issue, I only bring it up because it's a major issue in the only workaround I can see to this issue.
I found a terrible (but working) solution: define a property directly on an instance
// in beforeCreate-hook
const foo = computed(getter/setter)
Object.defineProperty(this, 'foo', {
enumerable: true,
configurable: true,
get: ()=> foo.value,
set: (v)=> foo.value=v
})