Create a global computed property in a plugin in Vue 3 - vue.js

I'm trying to create a global computed property from within a Vue 3 plugin, so that my property can be used reactively in any component. I am using the standard Vue 3 pattern:
app.config.globalProperties.$foo = ...
This works great to let me access this.$foo from any component. However, I also want to trigger behavior when this.$foo is set. But when I try to make it a settable computed property, like this—
app.config.globalProperties.$foo = computed({
get: () => ...
set: () => ...
})
—it doesn't behave like a computed property: If I run this.$foo = 'bar' from a component, it simply overwrites the property, without triggering the computed setter. (Testing it on a computed without a setter, the property is also simply overwritten, where Vue would normally throw a warning.)
How can I make a global computed with a setter? Is there something I am missing here about how global properties (or computed properties in the Vue 3 composition API) are supposed to work?

computed() returns a ref, so its value must be accessed through the .value property. Setting $foo directly would just overwrite the reference to some new value instead of modifying the computed ref's value:
this.$foo = 'bar' // ❌ overwrites the computed ref
this.$foo.value = 'bar' // ✅ sets computed ref's value; triggers setter
You likely need reactivity, and a computed ref with a getter/setter would need to use another ref for that:
// main.js
import { computed, ref } from 'vue'
⋮
let foo = ref(123)
app.config.globalProperties.$foo = computed({
get: () => foo.value,
set: value => foo.value = value,
})
// MyComponent.vue
export default {
methods: {
logFoo() {
console.log(this.$foo.value) // => 123
},
updateFoo() {
this.$foo.value++
}
}
}
demo

Related

Vue 3 reactivity of a primitive ref passed as a prop

While playing with Vue 3 reactivity I incountred a behaviour that I couldn't explain.
I created a ref of a primitive. Checking isRef on it returns obviously true. But when passed to a child component as a prop, isRef() and also isReactive() return false. Why ?
Also, even if they both return false, the watcher of this prop I added in the child component is triggered if the value changes. How could it trigger if the watched value is not a ref nor reactive ?
Here is a code snippet for both the parent and the child :
Parent.vue
let foo = ref(0);
function changeFoo() {
foo.value++;
}
console.log("parent check is reactive?", isReactive(foo)); // retruns false
console.log("parent check is ref?", isRef(foo)); // retruns true
watch(foo, (value, oldValue) => {
console.log("foo changing from", oldValue, "to ", value);
});
Child.vue
const props = defineProps<{
foo: number;
}>();
console.log("child check is reactive?",isReactive(props), isReactive(props.foo)); // returns true false
console.log("child check is ref ?",isRef(props), isRef(props.foo)); // returns false false // Question 1: Why props.foo is not a Ref ?
watch(props, (value, oldValue) => {
console.log("props changing from", oldValue, " to ", value);
});
watch(()=>props.foo, (value, oldValue) => {
// Question 2: Why this Watcher detects changes of "foo" without it being a ref nor reactive ?
console.log("props.foo changing from ", oldValue, " to ", value);
});
And a link to Vue SFC Playground here
Bonus question :
When foo is passed to a composable used in the child component, the watcher inside the composable is not triggered unless we pass foo via a toRef but we don't need this additional step if foo is a ref/reactive object. Why primitives needs this addition step ?
But when passed to a child component as a prop, isRef() and also isReactive() return false.
You're only telling half the story here, as your own comment actually indicates:
console.log("child check is reactive?",isReactive(props), isReactive(props.foo)); // returns true false
isReactive(props) does return true, because the entire props object (which wraps foo) is reactive. Any update the parent makes to foo, gets passed down to the Child as an updated props object. It's true props.foo is not a ref/reactive, because it doesn't need to be. As long as props is reactive, props.foo will update
The watcher is able to activate on changes to props.foo because you're actually using special syntax where you pass in a "getter" to the watch specifically meant for watching the property of a reactive object (in your case: props). There's an example in the Vue docs that says the same thing.
If you ever needed to assign props.foo to it's own reactive variable, say to pass to a composable, that's where toRef can be used.
// reactive but not synced with props.foo
const newFoo = ref(props.foo)
// reactive AND synced with props.foo
const newFoo = toRef(props, 'foo')
As I indicated with the above comments, if you make a new variable with just ref, it'll be reactive, but it won't be synced with it's source property, i.e. the first newFoo won't reactively update when props.foo updates, but the second newFoo will. This is also explained well in the Vue docs
Simple answer is that in the child, 'props.foo' is reactive when used within the setup/script (ie not in the template according to my playing arounds) but 'foo' isnt reactive if prop is destructured (ie const {foo} = props).
But you should do this by using the toRef function ( const foo = toRef(props,'foo')) to be extra sure OR make the entire props object reactive with toRefs (const reactiveProp = toRefs(props)) then refer to it with reactiveProp.foo (and it will still be reactive even if you destructure it) OR use computed eg const reactiveFoo = computed(()=>props.foo).
You can play around (and see my commented notes here Vue Props Reactivity Playground).
Frustrated me for a while cos this is unlike React.js (which I had been using for a long time), the props are reactive (even if destructured). See test here React Props Reactivity

How to tell whether a prop has been explicitly passed in to a component in Vue 3

Is there any way to tell, from within a Vue 3 component, what props are being explicitly passed in (i.e., as attributes on the component's tag in the parent template)—as opposed to being unset and receiving their value from a default?
In other words, if I have a prop declared with a default value, like this:
props: {
name: {
type: String,
default: 'Friend'
}
}
How can I tell the difference between this:
<Greeting name="Friend" />
and this:
<Greeting /> <!-- name defaults to 'Friend' -->
In both instances, the component's name prop will have the same value, in the first case being passed in explicitly and in the second case being assigned the default.
Is there a systematic way in Vue 3 to determine the difference?
In Vue 2 you could use this.$options.propsData for this purpose—it contained only the properties that were directly passed in. However, it has been removed in Vue 3.
This information may be available in component instance with getCurrentInstance low level API, but there is no documented way to distinguish default and specified prop value that are === equal.
If default prop value is supposed to differ from the same value that was specified explicitly, this means that this isn't the case for default prop value, and it should be specified in some other way, e.g. through a computed:
const name = computed(() => {
if (props.name == null) {
console.log('default');
return 'Friend';
} else
return props.name;
});
There is a way, I believe, although since it's a pretty specialised requirement, we need to directly access Vue's undocumented methods and properties, which is not considered stable (although usually fine).
<script setup>
import { getCurrentInstance, computed } from "vue";
const props = defineProps({
test: {
type: String,
default: "Hello"
}
});
const vm = getCurrentInstance();
const propDefaults = vm.propsOptions[0];
const testIsDefault = computed(() => propDefaults.test.default === props.test);
</script>

Behaviour of Vue-observable, when adding properties after creation

I'd like to have a little registry in one of my Vue files and decided to use the Vue.observable function Vue provides (yes I could use Vuex, but first I want to try without). Now, when I add properties to my registry after creation (using Vue.set of course), I find that the reactivity of my properties is hard to predict:
This does work as expected:
const state = Vue.observable({fromObservable: ''})
...
computed:
fromObservable: () => state.fromObservable
...
In mounted:
state.fromObservable = 'Success'
This sadly does not work. Why?
const state = Vue.observable({})
...
computed:
fromObservable: () => state.fromObservable
...
In mounted:
Vue.set(state, 'fromObservable', 'Success')
Nested properties work as expected:
const state = Vue.observable({values: {}})
...
computed:
fromObservable: () => state.values.fromObservable
...
In mounted:
Vue.set(state.values, 'fromObservable', 'Success')
It does not work, if I instantly assign values to a variable. I have no clue, how this is happening:
const state = Vue.observable({values: {}}).values
...
computed:
fromObservable: () => state.fromObservable
...
In mounted:
Vue.set(state, 'fromObservable', 'Success')
Here is a fiddle demonstrating this.
Please explain to me, how this can be understood, especially case 2 and 4. Thanks in advance for your time.
First, Vue2 makes objects reactive using Object.defineProperty
It's safe to say that not object itself is reactive, it's properties are
Second important fact is that computed properties:
Track reactive dependencies while evaluating
dependency is any reactive property accessed during function evaluation
cache the value and doesn't recompute unless some reactive dependency has changed
Case 2
when computed is called, fromObservable property of state does not exist
computed returns undefined
not a single getter was accessed!! So list of dependencies which should trigger the recompute is empty (in other words, this computed will never re-evalute again)
Case 3
state.values getter is accessed in computed property so it's a dependency
when Vue.set is called with state.values as an argument, it sees values is reactive property (has setter and getter added by Vue) so it registers addition of new property into object it's holding like a change
as a result, computed property will re-evaluate on next render
Case 4
almost like Case 2 because inside computed property, no getter is accessed and returned value is undefined
only time getter is accessed is in const state = Vue.observable({values: {}}).values (and returns empty object which is not reactive by itself)
from perspective of Vue, this computed property is constant because it has no reactive dependencies
Luckily for all of us, all this reactivity caveats are fixed in Vue 3 thanks to reactivity system rewritten using proxies - here is your code rewritten to use Vue 3 - with all cases working!

Computed function running without to call it

I'm setting an array in my data property through a computed function and it's working. But I wonder how is possible if I don't call it anywhere?
If I try to add a console.log in my function it doesn't print anything, but it's still setting my data, how is that possible?
My data:
data() {
return {
projects: []
};
},
My computed:
computed: {
loadedProjects() {
console.log("Hello there")
this.projects = this.$store.getters.loadedProjects
}
},
I expect that it doesn't run because I'm not calling, and if it is running(I don't know why) to print the console.log before to set my data. Any clarification?
Thanks:)
You're confusing computed props with methods. If you want to have a method like above that sets a data value of your vue instace, you should use a method, not a computed prop:
data() {
return {
projects: []
};
},
methods: {
loadProjects() {
console.log("Hello there")
this.projects = this.$store.getters.loadedProjects
}
}
This would get the value of this.$store.getters.loadedProjects once and assign it to your local projects value. Now since you're using Vuex, you probably want your local reference to stay in sync with updates you do to the store value. This is where computed props come in handy. You actually won't need the projects in data at all. All you need is the computed prop:
computed: {
projects() {
return this.$store.getters.loadedProjects
}
},
Now vue will update your local reference to projects whenever the store updates. Then you can use it just like a normal value in your template. For example
<template>
<div v-for='item in projects' :key='item.uuid'>
{{item.name}}
</div>
</template>
Avoid side effects in your computed properties, e.g. assigning values directly, computed values should always return a value themselves. This could be applying a filter to your existing data e.g.
computed: {
completedProjects() {
return this.$store.getters.loadedProjects.filter(x => x.projectCompleted)
},
projectIds() {
return this.$store.getters.loadedProjects.map(x => x.uuid)
}
}
You get the idea..
More about best practices to bring vuex state to your components here: https://vuex.vuejs.org/guide/state.html
Computed props docs:
https://v2.vuejs.org/v2/guide/computed.html
You should check Vue docs about computed properties and methods
and shouldn't run methods inside computed property getter
https://v2.vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods
Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed.

What is the main difference between computed and mounted in vue.js 2?

when i add computed() instead mounted() it throws an error
export default {
components: {
MainLayout
},
mounted(){
var x = document.getElementById('homeTabPanes');
// x.style.background = "blue";
console.log("check the value of x", x);
}
}
computed is an object containing methods that returns data, mounted is a life hook executed after the instance gets mounted, check out the links to the docs it have really good explanation
From the docs
..computed properties are cached based on their dependencies. A computed property will only re-evaluate when some of its dependencies have changed.
If you want data to be cached use Computed properties on the other hand mounted is a lifecycle hook, a method which is called as soon as the Vue instance is mounted on the DOM.
In-template expressions are very convenient, but they are meant for simple operations. Putting too much logic in your templates can make them bloated and hard to maintain.
That’s why for any complex logic, you should use a computed property.
Basic Example
<div id="reverseMessageContainer">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
look at the js below:
var vm = new Vue({
el: '#reverseMessageContainer',
data: {
message: 'Hello'
},
computed: {
// a computed getter
reversedMessage: function () {
// `this` points to the vm instance
return this.message.split('').reverse().join('')
}
}
})
Here we have declared a computed property reversedMessage. The function we provided will be used as the getter function for the property vm.reversedMessage:
You can open the console and play with the example vm yourself. The value of vm.reversedMessage is always dependent on the value of vm.message.
console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'
For more better understanding you can visit
https://v2.vuejs.org/v2/guide/computed.html