Vue 3 reactivity of a primitive ref passed as a prop - vue.js

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

Related

How can I reset the value of a ref and keep an associated watcher working?

UPDATE:
I have achieved the desired behavior in the MCV by changing resetArray:
function resetArray() {
// myArray.value = [] // old version
myArray.value.length = 0 // new version
}
But I still don't understand why my MCV doesn't work.
ORIGINAL POST:
Background
In an app I am building, I store data in an a ref, created as const myArray = ref([]), which takes the form of an array of objects. This array is only changed in the following ways:
myArray.value[index] = {key: value}
myArray.value = [].
In particular, at no time is an object in myArray modified, it is either created or replaced.
I later added a watch which took action on every change to myArray.value. I discovered that after resetting myArray to [], the watcher stopped getting called.
Things I have tried:
I confirmed that my usage of ref follows the guidelines in this SO answer regarding ref vs reactive.
Refactoring to use watchEffect instead of watch. Did not help.
Refactoring to use reactive rather than ref. Did not help.
My Issue
In the MCV below, modifying myArray by calling addToArray works as intended: myArray.length is rendered and the first watch is triggered.
Calling resetArray triggers only the second watch, but the first watch IS NOT triggered when addToArray is called afterwards.
My Question
How can I both keep the ability to set myArray to [] and trigger actions every time myArray changes?
My MCV
View my MCV on Vue SFC Playground
The below code is the content of App.vue in a Vue project created with npm init vue#latest:
<script setup>
import {ref, watch} from "vue"
const myArray = ref([])
function addToArray() {
myArray.value.push("1")
}
function resetArray() {
myArray.value = []
}
watch(myArray.value, () => {
console.log("CLICKED!")
})
watch(myArray, () => {
console.log("RESET! clicked won't get called again!")
})
</script>
<template>
{{myArray.length}}<br />
<button #click="addToArray">CLICK ME</button><br />
<button #click="resetArray">RESET</button>
</template>
When watching a ref, use the ref itself -- not its value property -- as the watch source (the 1st argument to watch()).
To observe new array assignments or item additions/removals, pass the deep:true option (the 3rd argument to watch()):
watch(
myArray 1️⃣,
() => { /* handle change */ },
{ deep: true } 2️⃣
)
demo

Create a global computed property in a plugin in Vue 3

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

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!

watch props update in a child created programmatically

I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = dataFn() || {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach(key => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".

Vue: How to iteratively update properties in props the correct way

I'm creating a component that updates props with values from local storage. The props are objects with multiple boolean properties (e.g. this.globalStates.repeat = false). Because I have more than one prop to update, I've created a method for any prop provided as an argument:
mounted(){
this.loadLocalData(this.globalStates, "states")
this.loadLocalData(this.globalSettings, "settings")
},
methods: {
loadLocalData(object, localVariable){ // 'object' receives a prop, 'localVariable' must refer to a string in localStorage (localStorage stores strings only).
const loadedObject = JSON.parse(localStorage.getItem(localVariable)) // Turn localStorage string into JSON object
if (loadedObject && typeof loadedObject === "object"){
for (let item in object){ // iterate through the prop and update each property with the one in loadedObject
object[item] = loadedObject[item] // Why does this work!?
}
}
}
},
Now this actually works, without errors or warnings. I don't understand why though. Normally when I try to modify a prop directly, Vue throws a warning at me:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
Instead of object[item] = loadedObject[item] I tried the following, but that actually failed:
const u = 'update:' + object + '.' + item
this.$emit(u, loadedObject[item] )
What would be the correct way to do this?
The correct way would be to not sync objects, because sync does not support that: https://github.com/vuejs/vue/issues/6241
Alternative solutions are:
Put the method in the parent
Sync each property as a separate prop
Use a data store: https://v2.vuejs.org/v2/guide/state-management.html