VueX Getter acting reactively - how to assign non-reactive to variable? - vue.js

I am running into a bit of a quirk that I can't seem to wrap my head around and don't know if this is the standard behavior of VueX or if I'm implementing this incorrectly.
I have a VueX getter on a modular store that I am assigning to a new variable. When I attempt to manipulate the newly assigned variable, it also updates state - how can I just create a non-reactive duplicate of the getter result?
Here is my VueX code:
export const getters = {
getActiveFilters: (state) => {
return state.filters.filter(filter => {
return filter.status === 'active';
});
},
};
I then have a method that uses the function in a dynamic class comparator - essentially it returns true or false as to whether or not a list of items is empty with the addition of a new filter.
<ul>
<li
v-for="filter in $store.getters['filters/getActiveFilters']"
:key="filter.id"
:class="{disabled: !isSelectable(filter.name, filter.value)}"
>{{ filter.name }}</li>
</ul>
Here is the method that is doing a bit of calculation / manipulation before returning a bool:
isSelectable(name, value) {
const activeFilters = this.$store.getters['filters/getActiveFilters'];
/* Here's the part that is screwing up and manipulating the getter result */
activeFilters.push({name, value, id: 'test-id'});
let valid = false
this.activeFilters.forEach(filter => {
// valid = true;
});
return valid;
},
console.log(this.$store.getters['filters/getActiveFilters']) will return the results with the new value added, even though it was added to a different variable - so in the instance of using this in v-for - the list continually gets bigger every time.
Any idea on how to resolve?

The getter $store.getters['filters/getActiveFilters'] will return same(cached) result(reference to array) if there is no changes in state.filters. So when you push some element you push it to the cached filtered array that is referenced in other parts of app.
Just copy array with slice to avoid changing cached value:
const activeFilters = this.$store.getters['filters/getActiveFilters'].slice();
P.S. This is just shallow copy, so if you want to change some item property - use mutations.

You can use this approach to the problem:
const activeFilters = [...this.$store.getters['filters/getActiveFilters']]
This method create copy of array and this copy is not associated with vuex objects.
For save changed values, you must use mutations.
God luck.

Related

Can't have a Vue shortcut (by reference) to a reactive variable?

I have a Pinia store with an object "objData", which holds one or more objects, with some additional metadata, which ends up becoming a fairly long variable. It has to be used in quite a number of places, therefore I made a "shortcut" variable instead to the "data" property. However, this shortcut fails to be reactive, whereas the variable i'm pointing to is reactive.
The Pinia object looks like:
objData: {
"fruit": {
data: {...},
...
},
"candy": {
data: {...},
...
},
}
The setup-function:
setup() {
const myStore = useMyStore()
// const fruit = myStore.objData['fruit'].data // <- direct, doesn't work
// const fruit = reactive(myStore.objData['fruit'].data) // <- reactive, doesn't work
const fruit = computed(() => myStore.objData['fruit'].data) // works
return {
myStore,
fruit,
}
}
The data change: (I'm sure I don't need to both do reactive() and refs(), or any at all, but I've tried all kinds of things to get reactivity in my shortcut). This happens in a composable that has access to the store.
if (!("fruit" in store.objData)) {
set(myStore.objData, "fruit", reactive({
data: ref(null),
}))
}
set(myStore.objData["fruit"], 'data', objNewData)
The page:
<div>
{{myStore.objData['fruits'].data.fruit_name}} OK
{{fruit.fruit_name}} OK, if computed(), otherwise not reactive
</div>
Unless I'm using a computed, I only get the inital value, which doesn't get updated when the store updates.
Is it actually bad/expensive/wrong to use a computed() to have a reactive data object in the page in this way? It "feels" wrong, but other than that I have no arguments against it.
(Why) is it not possible to simply make a variable by reference to a reactive variable, I always thought you're just pointing to a memory address.
I'm struggeling to provide an example, as this thing is so deeply integrated in my app. I'm at this point hoping for a glaring mistake on my part, or a simple answer that explains it.
Note 1: I'm using Vue2 with the composition API add-on.
Note 2: This is a very simplified example.

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

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...

How do I use computed with nested Vuex Object

I have an object in Vuex which has the following functionality:
// Define the object in the state
myObj = {};
// Add a key and give it an empty array
state.myObj[someNumericalId] = [];
// Add a value to the array
state.myObj[someNumericalId].push['someData'];
How do I get notified of changes in computed when:
a key is added to the object
a value is added/removed from any array
Do I need to use a watch or can this be solely done through computed? A quick an dirty example to see if I get anything back would be:
<span v-for="(someArray, someIndex) in getMyObj" v-bind:key="someIndex">
<p> {{someIndex}} - {{JSON.stringify(someArray)}} </p>
</span>
computed: {
...mapGetters(['getMyObj'])
}
No need for a watch, here are two ways to make the property reactive:
Option one: Declare the property in your state object:
myObj = { someNumericalId: [] };
Option two: Use Vue.set to add the property to your object in mutations:
Vue.set(state.myObj, someNumericalId, []);
After this, push method should automatically trigger updates.

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
}
}
}

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.