Vuejs - add property to each item in an array with reactivity - vuex

I have banged my head against all kinds of walls for days and would love some help with this please.
I am getting the following error but can't really see how what I'm doing has anything to do with vuex here:
[vuex] do not mutate vuex store state outside mutation handlers.
In my vue component I define an empty array for future data I get from an external API.
data() {
return {
costCentres: [],
};
},
I have a watch on my vuex store object named schedule (which I've brought in using MapState... and also tried with a getter using MapGetter to see if that made any difference). In this watch I create my array costCentres, with each element consisting of about five properties from the API. I add two properties at this point (sections and tasks) which I intend to later populate, and which I need to be reactive so I do so in accordance with the Vue reactivity documentation which all the other questions I've found remotely like mine seem to reference.
watch: {
schedule() {
if (this.schedule.rows) {
this.costCentres = this.schedule.rows.filter((row) => {
return row.cells[
this.schedule.columnKeysByName["Cost Code"]
].value; // returns row if Cost Code value exists
});
this.costCentres.forEach((costCentre) => {
this.$set(costCentre, 'section', null);
this.$set(costCentre, 'task', null);
});
}
},
The this.$set lines throw the earlier mentioned error for every element in the array.
When I later update the properties, the change is reactive so its just the flood of error messages that's got me beat. Obviously if I don't use set then I don't get reactivity.
I have no idea how what I am doing is related to the vuex store as costCentre is a plain old data property.
I've tried hundreds of variations to get this all work (including this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 }) which doesn't seem to work) and I've run out of options so any assistance would be greatly appreciated!
(Please also let me know if I need to show more code - I was trying to keep this concise!)

this.schedule.rows is an array containing some objects (mapped from Vuex so the array and objects inside "belongs" to Vuex)
You are creating this.costCentres by filter - so in the end this.costCentres is just another array containing subset of objects from this.schedule.rows (elements inside are just pointers to objects inside Vuex)
In the forEach loop, you are modifying objects which are part of the Vuex store and as a result getting error [vuex] do not mutate vuex store state outside mutation handlers.
If you want to modify those objects, only way is to use Vuex mutations
Alternative solution is to make a copy of objects (create new objects with same values):
this.costCentres = this.schedule.rows.filter((row) => {
return row.cells[this.schedule.columnKeysByName["Cost Code"]].value;
})
.map((row) => ({...row, section: null, task: null }))
Note: code above creates just a shallow copy so if your objects does contain some deeply nested properties, you have to use some other way to clone them
Now objects inside this.costCentres are not part of the Vuex and can be modified freely without using mutations...

Related

Vue.set() and push() for Vuex store

I have a mutator that attempts to make the follow update:
state.forms[1].data.metrics.push(newArrayItem)
forms is an object with 1 as a key
metrics is an array
For some reason, Vuex successfully updates, but components don't react to this change.
I was reading about Vue.set() on https://v2.vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats
But I'm not sure how to apply it, or even if it's the right answer at all here.
Thanks for the help.
this.$forceUpdate is working, which is a bit strange because the component loading the data uses computed properties.
Form state is setup initially like so:
const state = {
forms: {}
};
New forms are pushed like so:
state.forms[id] = { formName: payload.formName, data: {metrics: []}};
And new metrics are added like so:
var result = getParent(state, payload.path);
result.metricParent.data.metrics.push({ id: newMetricId(state, result.formId), ...payload.metric });
Your problem is not with pushing to arrays. Your problem is in this line:
state.forms[id] = { formName: payload.formName, data: {metrics: []}};
Because you are creating a new property of state.forms without using Vue.set(), the form is not reactive, so when you push to the metrics array later, it's not recognized.
Instead, you should do this:
Vue.set(state.forms, id, { formName: payload.formName, data: {metrics: []}});
Then, pushing to state.forms[id].data.metrics should work as expected.
Vue setup reactive data looking for how the state/data is setup, by example if in a regular component you define the data like {x: {y: 10}} and you change the data somehow this.x.y = 20; it’s going to work, because Vue make the object with that structure reactive (because is the setup structure) based on that if you try to do, this.x.z = 10; not works because “z” not exists, and you need to tell to Vue that you need to make it reactive, this is when this.$set(this.x, “z”, 10); enters, it’s basically saying “make this data reference in position ‘z’ reactive”, after this point direct calls to this.x.z = ? works, in vuex the same happens, use Vue.set(state.forms, 1, { formName: payload.formName, data: {metrics: []}}); after that the reference to state.forms[1] (including sub data) is now reactive!

Vue - same mutation refreshes (or not!) components depending on which component it is called from?

I have problem understanding why THE SAME mutation fails to refresh data displayed in components (although it does change underlying vuex store data!) if it is called from one of the components, but it does refresh the data if called from another component?
I am updating Filter objects stored in store this way: state.report.filters[], where filters is array of Filter objects.
const state = {
report: {
filters: [], // array of Filter objects
...
}
}
My mutation looks for a filter in the array and substitutes the whole Filter object.
const mutations = {
setFilter: (state, newFilterValue) => {
let changedFilter = state.report.filters.find(filter => {
return filter.fieldName === newFilterValue.fieldName;
});
changedFilter = newFilterValue;
}
}
The mutation is called from a method of Filter class defined like this (separate module):
import { store } from './store';
export class Filter {
constructor ({
...
} = {}) {
this.operators = []; // Array of Operator objects
this.value = []; // Array of values - in this case Dates
};
updateOperator (operatorName) { // this mutation refreshes components when executed
this.operator[0] = new Operator(operatorName);
store.commit('setFilter', this); // whole object passed to the mutation
};
updateValue (newValue) { // this mutation changes store value, but fails to refresh components
this.value[0] = newValue; // newValue is a Date
store.commit('setFilter', this);
};
};
The app displays data in rows (each Filter has a separate row), each row contains cells, of which one contains components dedicated to Filter's value and Operator. These dedicated components receive as props callback functions which are methods of the Filter object. They execute the callback functions when a new value is entered passing the value to the Filter which then updates a relevant property and calls the mutation passing in both cases the whole Filter object as payload.
// TABLE CELL COMPONENT displaying filter value and operator
<template>
<td>
<operator-component
:iconName="proppedFilterObject.operator.iconName"
:callback="proppedFilterObject.updateOperator.bind(proppedFilterObject)"
></operator-component>
<value-component
:date="proppedFilterObject.value[0]"
:callback="proppedFilterObject.updateValue.bind(proppedFilterObject)"
></value-component>
</td>
</template>
<script>
export default {
props: ['proppedFilterObject'] // whole filter object
};
</script>
// OPERATOR COMPONENT
<template>
<div #click.stop="chooseOperator">
{{ iconName }} // some operator value display
</div>
</template>
<script>
export default {
methods: {
chooseOperator () {
const modal = new ChooseOperatorModal({
callback: this.callback // this displays another modal for receiving data. The modal calls the callback.
});
},
},
props: ['callback', 'iconName']
};
</script>
// VALUE COMPONENT
<template>
<date-picker v-model="computedDate"> // THIRD PARTY COMPONENT
</date-picker>
{{ date }} // additional display to verify if there's a problem within 'date-picker'
</template>
<script>
import DatePicker from 'vue2-datepicker'; // THIRD PARTY COMPONENT
export default {
components: { DatePicker },
computed: {
computedDate: {
get: function () {
return this.date;
},
set: function (newValue) {
this.callback(newValue);
}
}
},
props: ['callback', 'date']
};
</script>
So, if eg. I enter new operator value from Operator component, everything refreshes. When I enter a new value in the value component, the mutation is executed and store value changed, but displayed data are not refreshed. However, if afterwards I change an operator all the components will refresh and value will get displayed. Even if I change operator in a different Filter object(!). Ie:
a) Change in report.filters[0].value - display not refreshed, but...
b) then change report.filters[1].operator - both report.filters[1].operator AND PREVIOUSLY CHANGED report.filters[0].value get refreshed(?!).
What can be a reason of such behaviour? Where to look for the problem?
Some additional remarks:
1) I am using a third party component "vue2-date-picker" for date choice and display. However it does not seem to be responsible for the problem, as if I try to display the new value just in {{ }} notation it behaves the same way. I have used the date picker in other components and there it functions correctly as well.
2) In the code samples I left out most imports/exports and other seemingly irrelevant elements to keep the question reasonably short.
There are a lot of problems with the code and several of them are contributing to the problems you're seeing. A full, thorough answer that addresses all of these problems would be ridiculously long so instead I will skim through them without going into huge amounts of detail. You will need to do some further reading and experimentation to understand each of these topics properly.
Let's start with this line in the mutation:
changedFilter = newFilterValue;
This line assigns a new value to the local variable changedFilter. That's all. As it's the last line of the mutation the net result is that it doesn't really do anything.
Presumably your intent was to update the array state.report.filters, replacing the old entry with a new entry. However, just updating a local variable isn't going to do that.
At this point you may be wondering 'If that doesn't do anything, then why is the state in my store changing?'. I'll come to that in a moment but first let me prove to you that your existing code does nothing.
Try removing the code inside setFilter completely. Just leave an empty function. Then try clicking around in the UI just like you did before. You'll find that the store state updates just the same as it did before, even though you've removed the code to update the array.
The correct way to implement that mutation would be to use findIndex to find the relevant index and then use either Vue.set or the array's splice method to update the array accordingly. That will change the item in the array. However...
This brings us back to the earlier question. Why is the state updating if the mutation does nothing?
This is because you're using the same object in multiple places. The Filter object held in the array is the same object that your UI is editing. There are no copies being taken, there is just a single object. So when you change the properties of that object inside updateOperator or updateValue this will immediately be reflected inside the store. Calling the setFilter mutation is just asking the store to replace an object with itself.
There's nothing specific to Vue about this. This is just the standard behaviour of reference types in JavaScript. It is also common with many other programming languages that don't directly expose pointers. It can be useful to learn a little about how pointers work in other languages as it will give you a better initial mental model before attempting to understand how reference types behave in JavaScript. Understanding the difference between 'by value' and 'by reference' may also be a useful starting point.
The next topic to cover is reactivity, which very much is a Vue topic.
Specifically, there are certain changes that Vue can't detect. These are usually referred to as the reactivity caveats. You can find more about them in the official documentation:
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
https://v2.vuejs.org/v2/guide/list.html#Caveats
There are at least two lines in your code that violate these rules:
this.operator[0] = new Operator(operatorName);
and
this.value[0] = newValue;
You can't set array entries directly by index. The array will update but it won't trigger any reactive dependencies within Vue. Instead you need to use either Vue.set or one of the array methods, e.g. push, pop, splice, etc.. In this example you could use splice.
e.g. Using Vue.set:
Vue.set(this.value, 0, newValue);
e.g. Using splice:
this.value.splice(0, 0, newValue);
Why does all of this matters?
Well Vue will only re-render a component if its reactive dependencies have changed. They are very similar to computed properties in that regard. Here's how it works...
Vue compiles the template down to a function. That function is referred to as the render function. When rendering a component Vue calls the render function and that function returns a description of how to render the component. Any reactive properties that are touched while that function is running will be recorded as dependencies. If, at some point in the future, the value of one of those reactive properties changes then Vue will rerun the render function to generate a new rendering of that component.
There are two key points to take out of this description:
If you fall foul of one of the reactivity caveats then Vue won't know the dependency has changed, so it won't re-render the component.
The render function runs as a whole. It doesn't just target a small chunk of the template, it always runs the whole thing.
So if you change a dependency in a non-reactive way (i.e. one of the caveats) it won't trigger a rendering update. But if you subsequently update a dependency properly, Vue will detect that and will rerun the render function. When it runs it will run the whole thing, so any new values will be picked up, even if they weren't detected when they changed.
It isn't immediately clear to me which rendering dependency is causing your component to re-render. However, it only needs one of them to change in a detectable manner. Any other changes will then get pulled in incidentally when the render function runs and reads their current values.
That covers why your code isn't working. However, I would also worry about your decision to introduce a Filter class. I understand how that may be appealing if you've come from some other OO environment but it isn't typically how Vue is used. It is possible to make it work but you will need a good understanding of both JavaScript reference types and the Vue reactivity system to avoid falling through the cracks. There is no reason why using a specific class to hold your data can't be made to work but in practice it usually ends up being less maintainable than not using such a class. A more typical Vue approach would be to use simple, anonymous objects/arrays to hold the data and then for the data owner (either a component or store module) to be responsible for making any mutations to that data. Events are used to pass changes up the component hierarchy rather than callback props.
Ultimately you will need to judge whether the Filter class is justified but it is probably not what future maintainers of your code will be expecting.

Dynamically adding an array of objects to Vue (reactivity problem)

I know that in order for an object or array to be reactive in Vue its properties have to be defined on the root data structure.
What's the best way to add an array of objects to a pre-existing variable defined on the root data structure, and make every property of every element in that array reactive?
I have tried looping through the array and adding each to the root data model, ie:
these_terms.forEach(function(term, idx) {
term.selected = false;
Vue.set(vm.game.set,idx,term);
});
However, Vue does still not respond to the "term.selected" property when it is later changed.
Is there a better way of achieving my aim, or do I need to resort to $forceUpdate? (the manual says that in 99% of cases using $forceUpdate, you're doing something wrong, hence this post)
On your parent component, do the following:
Make a data attribute with a empty array starting out
Make a button that calls a method
In that method, push to the empty array.
Example of step 3
methods: {
_addGroup: function() {
let result = {
id: this.wizardGroups.length + 1,
name: '',
};
this.wizardGroups.push(result);
},
If you need to append additional properties afterwards, you can loop through the array of objects and apply Vue.set() as well
Sorry if I understand it wrong but why dont you import the array and bring it into a Vue Data Variable?
import xx from "xxxx.js"
export default {
data() {
return {
y: xx
}
}
}

How to access Vuex modules mutations

I read thorugh his documentation from vue but I didn't find anything about how to actually access specific module in the store when you have multiple modules.
Here is my store:
export default new Vuex.Store({
modules: {
listingModule: listingModule,
openListingsOnDashModule: listingsOnDashModule,
closedListingsOnDashModule: listingsOnDashModule
}
})
Each module has its own state, mutations and getters.
state can be successfully accessed via
this.$store.state.listingModule // <-- access listingModule
The same is not true for accessing mutations cause when I do this
this.$store.listingModule.commit('REPLACE_LISTINGS', res)
or
this.$store.mutations.listingModule.commit('REPLACE_LISTINGS', res)
I get either this.$store.listingModule or this.$store.mutations undefined error.
Do you know how should the module getters and mutations be accessed?
EDIT
As Jacob brought out, the mutations can be accessed by its unique identifier. So be it and I renamed the mutation and now have access.
here is my mutation:
mutations: {
REPLACE_OPEN_DASH_LISTINGS(state, payload){
state.listings = payload
},
}
Here is my state
state: {
listings:[{
id: 0,
location: {},
...
}]
}
As I do a commit with a payload of an array the state only saves ONE element.
Giving in payload array of 4 it returns me back array of 1.
What am I missing?
Thanks!
It's a good idea, IMHO, to call vuex actions instead of invoking mutations. An action can be easily accessed without worrying about which module you are using, and is helpful especially when you have any asynchronous action taking place.
https://vuex.vuejs.org/en/actions.html
That said, as Jacob pointed out already, mutation names are unique, which is why many vuex templates/examples have a separate file called mutation-types.js that helps organize all mutations.
re. the edit, It's not very clear what the issue is, and I would encourage you to split it into a separate question, and include more of the code, or update the question title.
While I can't tell why it's not working, I would suggest you try using this, as it can resolve two common issues
import Vue from 'vue'
//...
mutations: {
REPLACE_OPEN_DASH_LISTINGS(state, payload){
Vue.$set(state, 'listings', [...payload]);
},
}
reactivity not triggered. Using Vue.$set() forces reactivity to kick in for some of the variables that wouldn't trigger otherwise. This is important for nested data (like object of an object), because vue does not create a setter/getter for every data point inside an object or array, just the top level.
rest destructuring. Arrays: [...myArray] Objects: {...myObj}. This prevents data from being changed by another process, by assigning the contents of the array/object as a new array/object. Note though that this is only one level deep, so deeply nested data will still see that issue.

Dynamically creating a reactive array in the Vuex's state

My component would like to add a new reactive-array field to the SST (vuex). I tried in beforeCreate hook, but the added array is not reactive; it's just a plain JS array.
Note that this is not the same as adding/removing elements from an existing array created at the Vue's initialization time. Such arrays are "wrapped" and become reactive as expected, mindful of "Array Change Detection" gotchas.
In my case, I'm trying to dynamically add an entirely new field of array type to the SST and make it reactive at the same time. Possible?
Have a look at Reactivity in Depth - Change Detection Caveats:
Change Detection Caveats
Due to the limitations of modern JavaScript, Vue cannot detect property
addition or deletion.
Since Vue performs the getter/setter conversion process during
instance initialization, a property must be present in the data object
in order for Vue to convert it and make it reactive.
But you say you are adding an array dynamically:
I'm trying to dynamically add an entirely new field of array type to the SST and make it reactive at the same time. Possible?
From the docs (bold is mine):
Vue does not allow dynamically adding new root-level reactive properties to an already created instance. However, it’s possible to add reactive properties to a nested object using the Vue.set(object, key, value) method:
Vue.set(vm.someObject, 'myArrayName', [1,2,3]);
Which should help you making your array reactive.
I see two problems here:
add dynamically array using vuex.
add dynamically element to this array and render this element.
I've initiate array if not exist in add method because when I'm receiving data from server myArray is not exist.
My solutuion below:
myVuexArray.js
import Vue from 'vue'
const state = {
myObject: {
myArray: [],
}
}
const getters = {
getMyArray: state => {
return state.myObject.myArray;
}
}
const mutations = {
addElementToArray(state, value) {
if (state.myObject.myArray === null || state.myObject.myArray === undefined || state.myObject.myArray === '') {
// initiate array
state.myObject.myArray = [];
}
// add new element to array
Vue.set(
state.myObject.myArray,
state.myObject.myArray.length,
value
);
// creates a new array everytime this solves the reactivity issue
Vue.set(state, 'myObject.myArray', state.myObject.myArray);
return state.myObject.myArray;
},
removeElementFromArray(state, index) {
state.myObject.myArray.splice(index, 1);
}
}
export default {
state,
mutations,
getters
}
Best regards
Dynamic module registration could help you to achieve this :
https://vuex.vuejs.org/en/modules.html
This would allow you to dynamically register a new module containing your array field in the beforeCreate hook.