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.
Related
I just noticed an unexpected behaviour and now I don't know if it is normal or not.
I have a component named follows and a child component named follow-list-modal
I'm passing a followList (pagination ) from follows to its child component follow-list-modal
In the follow-list-modal I store the paginated array in the variable members
Follows.vue
<template>
<div>
<follow-list-modal
:follow-list="dataset">
</follow-list-modal>
</div>
</template>
<script>
export default {
props: {
dataset: {
type: Object,
default: {},
},
},
}
</script>
FollowListModal.vue
<template>
<div>
<button #click="fetchMore"> More </button>
</div>
</template>
<script>
export default {
props: {
followList: {
type: Object,
default: {},
},
data() {
return {
members: this.followList.data,
dataset: this.followList,
};
},
methods: {
fetchMore() {
let nextPage = parseInt(this.dataset.current_page) + 1;
axios
.get(this.dataset.path + '?page=' + nextPage)
.then(({ data }) => this.refresh(data))
.catch((error) => console.log(error));
}
},
refresh(paginatedCollection) {
this.dataset = paginatedCollection;
this.members = this.members.concat(...paginatedCollection.data);
},
}
When I click the button More in the follow-list-modal to get more data, I then want to append the new data to the members array.
The unexpected behaviour ( for me at least ). is that if I use push in the refresh method
this.members.push(..paginatedCollection.data);
It appends data not only to members but also to followList which is data that comes from the parent component follows
But if I use concat instead of push, it appends data only to members variable and not to followList
this.members = this.members.concat(..paginatedCollection.data);
Is this behaviour normal ?
I don't get why the followList changes when the members variable changes, I thought that reactivity is one way.
In other words, the members changes when the followList changes, but not the other way around
P.S I don't emit any events from follow-list-modal to follows or change the data of the follows component in any way from the follow-list-modal
In JavaScript, the properties of an Object that are also Objects themselves, are passed by reference, and not by value. Or you might say that they are shallow copied.
Thus, in your example, this.members and this.followList.data are pointing to the same variable in memory.
So, if you mutate this.members, it will mutate this.followList.data as well.
You could avoid this by doing a deep copy of the objects. The easiest method, and arguably the fastest, would be to use JSON.parse(JSON.stringify(obj)), but look at this answer for more examples.
data() {
return {
members: [],
dataset: [],
};
},
created() {
this.members = JSON.parse(JSON.stringify(this.followList.data));
this.dataset = JSON.parse(JSON.stringify(this.followList));
}
You instantiate your data with a direct link to the (initially undefined) property of your prop. This property is a complex entity like an Object (Arrays are Objects), and is thus called via reference. Since members references the same thing in memory as followList.data, when you're calling members, it will follow the reference to the same entity as followList.data. This doesn't have to do with Vue2 reactivity, but here's a link nontheless.
push mutates the array it is called on; it will follow the reference through members and change followList.data, updating its value when called through followList as well. Because the data key is not present on instantiation of the component, Vue can't watch it (just like you need to use Vue.set when adding a new key to a data object).
concat returns a new array of merged elements, and then replaces
the reference in members with the new array. Therefore from this point on you'll
no longer mutate followList.data, even with a push, as the reference has changed to a new entity.
When trying to set your initial members and dataset, I suggest using an initialization method that creates a clone of your followList and writes that to dataset, and running this on the created() or mounted() hook of your component lifecycle. Then create a computed property for members, no need to store followList.data thrice and potentially have dataset and members diverge.
In my root Vue instance, I have an array of objects with some data, which I use to render a set of components. These components have a watcher on the object of data provided to them, which is supposed to make an asynchronous call every time the object is updated.
The problem is that when I update a property of one of the objects in my array, the watcher is not called. It shouldn't fall into any of Vue's caveats because a) I'm not adding a new property, just updating an existing one and b) I'm not mutating the array itself in any way. So why is this happening? And how do I fix it?
My main Vue instance:
let content = new Vue({
el: '#content',
data: {
testData: [
{ name: 'test1', params: {
testParam: 1
} },
{ name: 'test2', params: {
testParam: 1
} },
{ name: 'test3', params: {
testParam: 1
} }
]
}
});
The code which I use to render my components:
<div id="content">
<div v-for="item in testData">
<test-component v-bind="item"></test-component>
</div>
</div>
And my component:
Vue.component('test-component', {
props: {
name: {
type: String,
required: true
},
params: {
type: Object,
required: true
}
},
data: function() {
return { asyncResult: 0 };
},
watch: {
params: function(newParams, oldParams) {
// I use a custom function to compare objects, but that's not the issue since it isn't even being called.
console.log(newParams);
if(!this.compareObjs(newParams, oldParams)) {
// My async call, which mutates asyncResult
}
}
},
template: `
<span>{{ asyncResult }}</span>
`
});
My goal is to mutate the properties of the params property of a given object and trigger the watcher to rerender the corresponding component, but when I try to mutate it directly it doesn't work.
Example (and the way I'd like my component to work):
content.testData[2].params.testParam = 5;
Unfortunately, it doesn't. Using Vue.set doesn't work either:
Vue.set(content.testData[2].params, 'testParam', 5);
The only thing I found which does work is to assign a new object entirely (which is not something I'd like to do every time I have to mutate a property):
content.testData[2].params = Object.assign({}, content.testData[2].params, { testParam: 5 });
I also tried using a deep watcher, as suggested in a similar question, but it didn't work in my case. When I use the deep watcher the function is called, but both newParams and oldParams are always the same object, no matter which value I set to my property.
Is there a solution to this that will allow me to mutate the array items just by setting a property? That would be the most desirable outcome.
First things first.
Using Vue.set isn't going to help. Vue.set is used to set the values of properties that Vue's reactivity system can't track. That includes updating arrays by index or adding new properties to an object but neither of those apply here. You're updating an existing property of a reactive object, so using Vue.set won't do anything more than setting it using =.
Next...
Vue does not take copies of your objects when passing them as props. If you pass an object as a prop then the child component will get a reference to the same object as the parent. A deep watcher will trigger if you update a property within that object but it's still the same object. The old and new values passed to the watcher will be the same object. This is noted in the documentation:
https://v2.vuejs.org/v2/api/#vm-watch
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
As you've noticed, one solution is to use a totally new object when performing the update. Ultimately, if you want to compare the old and new objects then you have no choice but to make a copy of the object somewhere. Taking a copy when mutating is a perfectly valid choice, but it's not the only option.
Another option would be to use a computed property to create the copy:
new Vue({
el: '#app',
data () {
return {
params: {
name: 'Lisa',
id: 5,
age: 27
}
}
},
computed: {
watchableParams () {
return {...this.params}
}
},
watch: {
watchableParams (newParams, oldParams) {
console.log(newParams, oldParams)
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<input v-model="params.name">
<input v-model="params.id">
<input v-model="params.age">
</div>
A few notes on this:
The computed property in this example is only creating a shallow copy. If you needed a deep copy it would be more complicated, something like JSON.stringify/JSON.parse might be an option.
The computed property doesn't actually have to copy everything. If you only want to watch a subset of the properties then only copy those.
The watch doesn't need to be deep. The computed property will create dependencies on the properties it uses and if any of them changes it will be recomputed, creating a new object each time. We just need to watch that object.
Vue caches the values of computed properties. When a dependency changes the old value is marked as stale but it isn't immediately discarded, so that it can be passed to watchers.
The key advantage of this approach is where the copying is handled. The code doing the mutating doesn't need to worry about it, the copying is performed by the same component that needs the copy.
As you said, you will need to use deep property in watch.
Using Vue.set you should remounting the entire object inside your array, like:
const newObj = {
name: 'test1',
params: {
testParam: 1,
},
};
Vue.set(yourArray, newObj, yourIndex);
Note you are setting some value inside your array and in this case the array contains objects.
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.
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
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.