Behaviour of Vue-observable, when adding properties after creation - vue.js

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!

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

Vue mutate prop correctly

I'm trying to create a simple component whose focus is to display an element in an array, but I'm having issues with Vue's philosophy.
As you may know, if a mutation on a prop is triggered, Vue goes crazy because it doesn't want you to update the value of a prop. You should probably use a store, or emit an event.
The issue is: that since I'm adding functionalities to my codebase (for instance the possibility to start again when I reach the last element of the array), it would be wrong to have an upper component be responsible for this management, as it would be wrong to ask an upper component to change their variable, given that my component is supposed to manage the array, so an emit would be a bad solution.
In the same way, given that I'm making a generic component that can be used multiple times on a page, it would be incorrect to bind it to a store.
EDIT: the reason why the prop needs to be updated is that the component is basically acting as a <select>
Am I missing an obvious way to set this up?
To give an example of my end goal, I'm aiming for a component looking like the one in the picture below, and I think a 2 way bind like in v-model would be more appropriate than having to set an #change just to say to update the value of the passed prop.
If you have a prop the correct way to update the value is with a sync, as in the following example
Parent:
<my-component :title.sync="myTitle"></my-component>
Child:
this.$emit("update:title", this.newValue)
Here is a very good article talking about the sync method.
By the other hand you can alter a Vuex state variable by calling a Vuex mutation when you change the value:
computed: {
title: {
// getter
get() {
return this.$store.state.title
},
// setter
set(newValue) {
this.setTitle(newValue) // Requires mutation import, see the methods section.
// Or without import:
this.$store.commit('setTitle', newValue);
}
}
},
methods: {
...mapMutations("global", ["setTitle"]) // It is important to import the mutation called in the computed section
}
In this StackOverflow question they talk about changing state from computed hook in Vue. I hope it works for you.

mapstate to dynamic state objects of vuex store from the component

I am trying to build a watchlist (data streaming program) by vue3 with vuex. When a watchlist component subscribes for a symbol it should receive updates for that symbol from the store. When removing the subscription from the component that particular component should not receive that state change after that. We cannot hardcode symbol names in store to mapstate from component for each individually since there can be hundreds. if we take all the symbols as an attribute of a single object and map the state to it it will be a performance overhead since not all watchLists are referring to all the symbols.
So my question is there any way to inject a dynamically changing array to mapstate?
In component->
computed: {
...mapState([this should change dynamically]),
},
in Store->
state : {
these states also should be dynamic
},
or is there any workaround in vue to achive this?
I have found an alternative way by using getters. When we are using that we don't have to map state at all.
I am returning a function from getters with an argument since Vuex getters don't accept arguments.
symbol: (state) => (symbol) => {
return state.payload[symbol]
}
and from the component, I am watching for that getter.
this.$store.watch(
(state, getters) => getters.allSymbols(currency),
(newValue, oldValue) => {
this.symbolObjects[symbol] = newValue;
// do something
},
);
the watch is triggering every time the currency update. Also using the Vuex watcher gives the benefit of accessing both old and new values.

Will vuex work properly with generic mutations that don't reference `state`?

I’m using vuex with a tree of data. It's reactive and works well because we have a corresponding tree of components. Since the structure is a tree, it’s common to want to mutate a deeply nested child object. The easy way to implement that is with a mutation that accepts the child in its payload:
removeLeaf(state, { child, leaf }) {
child.children = child.children.filter((i: any) => i !== leaf);
state = state; // silence warning
},
A different way would be to change the mutation to work like this:
removeLeaf(state, { child_, leaf }) {
let child = searchForChild(state, child_);
child.children = child.children.filter((i: any) => i !== leaf);
},
I’m happy with the first way. Are there any drawbacks to writing mutations that modify a child object of state by using payload instead of the state parameter?
I don't think it'll work properly.
Vue's and Vuex's reactivity system is based on Javascript Getter and Setter.
If you console.log the state in any mutation, you will see something like this:
The get and set is the getter and setter of your Vuex States.
Without the setter, Vue's reactivity system likely wouldn't work.
Try console.log state and child in your mutation. You will likely see that the child doesn't contain setter.
If the setter is not there, Vue wouldn't know that you have updated the state of child, and you will likely have reactivity problem.

Difference in Vue between data() and adding data in created()

Is there a difference between the following? I've seen examples doing both and am unsure why you would choose one over the other.
Vue.component('test', {
data() {
return { myDataA: 10 };
}
//vs
created() {
this.myDataB = 10;
}
}
Variables set in created() on this will not be reactive. In order for them to be reactive, you must define them in the object returned by data().
Example (note how the text does not change in the output):
https://jsfiddle.net/oyf4quyL/
in a component, there are three places where you can define you data:
data properties
computed properties
props
the created property is lifecycle hook in Vue. what this means, is that the Vue will run this function when the component is created. there are also other lifecycle hooks in Vue you can use, like mounted or beforeMount or beforeCreate and etc.
now with this in mind, let's answer your question.
when you define myDataA in data property, Vue will automatically create some "watchers" for this data property, so anytime that you set a new value to myDataA, anywhere that is using it, will be called again. but when you define a property directly on Vue instance (this), you will lose this "watchers" feature. (which by the way is just some getters and setters!)
so as i said, the best way and the correct way to define a data property is on any of the three places that i mentioned, based on your need. (because each of them has different use-cases that the others).