Vuex: Add Dynamic Property to Object without triggering Watchers on existing Properties - vue.js

I have a Vuex store with an object:
state: {
contents: {},
}
where I dynamically store contents by key:
mutations: {
updateContent: (state, { id, a, b }) => {
Vue.set(state.contents, id, { a, b });
},
}
and get them using:
getters: {
content: (state) => (id) => {
if (id in state.contents) return state.contents[id];
return [];
}
},
Let's say I have a component like this:
export default {
props: ["id"],
computed: {
myContent() {
return this.$store.getters.content(this.id)
}
},
// ...
}
How can I add dynamic properties using the mutation without triggering changes in components watching unchanged, already existant properties of state.contents?
Also see this fiddle.

If you want to watch inner property of objects, you can use deep watchers.
In your situation, i am assuming you're setting properly yor getters, setter and update methods. You should add this to your watchers:
// ...
watch:{
id: {
deep: true,
handler(newVal, oldVal){
console.log("New value and old value: ", newVal, oldVal")
// ... make your own logic here
}
}
}
Let me explain little bit more above code, when we want to watch inner property of any object in Vue, we should use deep and handler function to manipulate in every change. (remember that, handler is not a random name, it's reserved keyword)

I'm trying to figure it out by checking the fiddle: https://jsfiddle.net/ericskaliks/Ln0uws9m/15/ , and I have a possible reason to this behavior.
Getters and Computed properties are updated or reached when the observed objects or properties inside them are changed. In this case the content getter is "watching" the state.contents store property, so each time store.contents is updated, the content getter is called, then the computed property myContent() updates its value and increase the updated data property with this.update++.
So the components will always be updated if the state.contents property is updated, i.e. adding a new unrelated property.

One could use a global event bus instead of a vuex getter:
const eventBus = new Vue();
components can subscribe to the element they need:
watch: {
id: {
handler(n, o) {
if (o) eventBus.$off(o + "", this.onChange);
eventBus.$on(n + "", this.onChange);
this.$store.dispatch("request", { id: n });
},
immediate: true
}
},
and changes of which the components have to be notified are dispatched using an action:
actions: {
request(_, { id }) {
eventBus.$emit(id + "", this.getters.content(id));
},
updateContent({ commit }, { id, a, b }) {
commit("updateContent", { id, a, b });
eventBus.$emit(id + "", { a, b });
}
}
That way one can precisely control when which updates are fired.
fiddle.

It seems like Vue(x) can't do this out-of-the-box. But by adding an empty Observer (yes, this is a hack) you can make Vue-components temporarily nonreactive (based on this blogpost).
Basically - we have an Observer on the object itself and an Observer on each of the properties. We will destroy the Observer on the object. When doing so, we have to make sure, that when the getter is first called by a component it returns a reactive value rather then {}, because the getter can't observe adding a new property to the object anymore. Therefore, we add a touch-mutation initializing the object. This function needs the original Observer of the object to create Observers on its properties, causing one, but only one, unnecessary update of the component:
mutations: {
updateContent: (state, { id, a, b }) => {
Vue.set(state.contents, id, { a, b });
},
touch: (state, { id }) => {
if(id in state.contents) return
if (myObserver === null)
myObserver = state.contents.__ob__
state.contents.__ob__ = myObserver
Vue.set(state.contents, id, {});
state.contents.__ob__ = new Observer({});
}
},
The constructor can be obtained using:
const Observer = (new Vue()).$data.__ob__.constructor;
Our component has to call touch whenever the id changes:
props: ["id"],
watch: {
i: {
immediate: true,
handler() {
this.$store.commit("touch", { id: this.id })
}
}
},
computed: {
myContent() {
return this.$store.getters.content(this.id)
}
},
As you can see in this fiddle, adding new properties to the object doesn't trigger unnecessary updates anymore.

Related

Can you commit a mutation from a getter call?

My Vuex module looks something like the below snippet. I use a getter to get various values from the parent object based on some logic in the parameters, (e.g. removing some options from a dropdown box based on other values in the object), and for example if something in the state would cause the returned value to have the locked property set to true, I want to fire off a mutation call to update the main object. I could do this on the vue level (where I get the computed values) using a dispatch call, but I'd rather not have to maintain that dispatch call in every place my getter is used. Can I call the mutation from the getter call directly? I've tried doing it as shown below but it keeps telling me commit is not a function.
var module = {
state: {
myObj: {
propertyA: { /* object with multiple different children */ },
}
},
getters: {
getField: state => params => {
switch(params.option) {
/*logic based on current state values*/
var ret = state.PropertyA[params.Field];
if(ret.conditionalProperty) {
// update state.Property[otherKey]
commit('updateField', { myValue: true }); // The problem in question
}
return ret;
}
},
mutations: {
updateField(state, payload) { state.PropertyA.Field = payload },
}
}

How to handle object mutations in Vuex at once

I have an object in vuex:
state () {
return {
generalInfo: {
userName: 'Bla',
firtsName: 'Bla',
lastName: 'Bla'
}
}
}
I'm using this object in a component with the use of a computed property like so:
computed: {
generalInfo: {
get() {
return this.$store.getters['profile/generalInfo']
},
set(newVal) {
this.$store.commit('profile/setProfileInfo', newVal)
}
}
},
As you can see I've made a mutation in my store that handles the mutation of my object:
mutations: {
setProfileInfo(state, obj) {
state.generalInfo.userName = obj.userName
state.generalInfo.firstName = obj.firstName
state.generalInfo.lastName = obj.lastName
}
},
But for some reason vuex still keeps complaining about setting states outside a vuex mutation. When I do this per object item (i.e. I'm setting a computed property on generalInfo.UserName with a corresponding mutation handler) things work just fine but I don't want to do it on every seperate object item...
I'm using vuex4 (next)

Vue test watcher change but code inside not response

I'm stuck in this situation where I have a getters from Vuex Store, and whenever that getter change (new value update), the local state data(the 'list') should be reassign .
This is my component, which has 'list' in data
And here is my test Successfully change the getSkills to the getSkillsMock, but there is no response from list, list is still an []
You need to set deep to true when watching an array or object so that
Vue knows that it should watch the nested data for changes.
watch: {
getSkills: {
handler () {
this.list = this.getSkills.map(items => {
return { value: items.id, label: items.name }
})
},
deep: true
}
}

How to compute a property based on an object with fallback

I have a component that receives an object as prop, like this:
props: ['propObject']
Then, there's a default object defined (I use VueX, so it's actually defined as a $store getter, but to make it simpler, let's say it's defined in the data method) in the data:
data() {
return {
dataObject: {defaultValueA: 1, defaultValueB: 2}
}
}
And I'd like to have a computed property that would behavior like this:
computed: {
computedObject() {
return Object.values(this.propObject).length > 0 ? this.propObject : this.dataObject;
}
}
However, I know this is not possible because Vue watchers don't watch for changes in the key/value pairs of an object.
I have tried to go with a watched property, like this:
props: ['propObject'],
data() {
return {
object: {},
defaultObject: {}
}
},
watch: {
propObject: {
handler: function() {
this.setComputedObject();
},
deep: true
}
},
methods: {
setComputedObject() {
this.object = Object.values(this.propObject).length > 0 ? this.propObject : this.defaultObject;
}
},
mounted() {
this.setComputedObject();
}
However, the watcher handler is not being called at all when the propObject changes, but if I call it directly via console, it works. Is there any way that I can make the computedObject become reactive?
you need to use Vue.set/vm.$set where you change the props (in source component)
for example
changeProp(){
this.$set(propObject,'newprop','newval');
}
and then just you regualr compouted in the target component (the component which receive the prop)
source : https://v2.vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats

How to set computed property a value if it's not bound to component's data

I have a component that has a computed property which gets its value from the Vuex store, like following:
computed: {
details () {
return this.$store.getters.getDetails
}
}
getDetails getter returns an object with several properties.
Now the problem is, how to update properties of 'details' object in the component where it is defined?
If it was through the UI, then it could be done via v-model. But it's needs to be done via component's methods. Like the following:
methods: {
someMethod () {
// here I need to update props of 'details' object, but how?
}
}
Since you're using Vuex, do it in the store.
Let's say your details object is like this:
details: { foo: 1, bar: 2 }
Then add a mutation for modifying the details object in the state (I used and because I don't know whether you only want to modify a property of the details or actually want to assign a new object to it):
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
details: { foo: 1, bar: 2 }
},
getters: {
getDetails: (state, getters) => {
return state.details;
}
},
mutations: {
UPDATE_DETAILS (state, payload) {
this.$set(state.details, payload.key, payload.val)
},
REPLACE_DETAILS (state, payload) {
state.details = payload
}
}
});
Then in your component:
// ...
methods: {
// ...
updateDetails(key, val) {
this.$store.commit('UPDATE_DETAILS', { key, val });
},
replaceDetails(obj) {
this.$store.commit('UPDATE_DETAILS', obj);
}
}
Update: What I said is basically a longer explanation of what #Bert was trying to say in his comment.