How to handle object mutations in Vuex at once - vue.js

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)

Related

Vuex store error when updating existing store item

Little confused here...
I am trying to update a nested object in a Vuex state, which seems to work until I try to add another new value.
Here is my mutation
export const mutations = {
add(state, block) {
state.blocks.push({
type: block,
data: {},
style: {}
})
},
modify(state, [i, key, obj]) {
state.blocks[i][key] = Object.assign({}, state.blocks[i][key], obj)
},
remove(state, index) {
state.blocks.splice(index, 1)
console.log(index)
}
}
Actions:
export const actions = {
createBlock(context, type) {
context.commit('add', type);
},
updateBlock(context, payload) {
context.commit('modify', payload);
},
removeBlock(context, index) {
context.commit('remove', index)
}
}
And my dispatch of the action
this.$store.dispatch('updateBlock', [this.index, 'data', this.obj]) // works
this.$store.dispatch('updateBlock', [this.index, 'style', {m: newMargin}]) //throws error
When I update a Block with the type being data, things work, though when I try to add new data to the style object I get an error
[vuex] do not mutate vuex store state outside mutation handlers.
The end goal is to be able to add key/values to the styles object in the block. This will allow me to create dynamic class names.
What the heck am I missing? I feel like it has to do with Object.assign

Setting intial local data from Vuex store giving "do not mutate" error

I thought I understood the correct way to load inital state data from Vuex into the local data of a component, but why is this giving me “[vuex] do not mutate vuex store state outside mutation handlers.” errors! I am using a mutation handler!
I want my component data to start empty, unless coming back from a certain page (then it should pull some values from Vuex).
The component is using v-model=“selected” on a bunch of checkboxes. Then I have the following:
// Template
<grid-leaders
v-if="selected.regions.length"
v-model="selected"
/>
// Script
export default {
data() {
return {
selectedProxy: {
regions: [],
parties: [],
},
}
},
computed: {
selected: {
get() {
return this.selectedProxy
},
set(newVal) {
this.selectedProxy = newVal
// If I remove this next line, it works fine.
this.$store.commit("SET_LEADER_REGIONS", newVal)
},
},
},
mounted() {
// Restore saved selections if coming back from a specific page
if (this.$store.state.referrer.name == "leaders-detail") {
this.selectedProxy = {...this.$store.state.leaderRegions }
}
}
}
// Store mutation
SET_LEADER_REGIONS(state, object) {
state.leaderRegions = object
}
OK I figured it out! The checkbox component (which I didn't write) was doing this:
updateRegion(region) {
const index = this.value.regions.indexOf(region)
if (index == -1) {
this.value.regions.push(region)
} else {
this.value.regions.splice(index, 1)
}
this.$emit("input", this.value)
},
The line this.value.regions.push(region) is the problem. You can't edit the this.value prop directly. I made it this:
updateRegion(region) {
const index = this.value.regions.indexOf(region)
let regions = [...this.value.regions]
if (index == -1) {
regions.push(region)
} else {
regions.splice(index, 1)
}
this.$emit("input", {
...this.value,
regions,
})
},
And then I needed this for my computed selected:
selected: {
get() {
return this.selectedProxy
},
set(newVal) {
// Important to spread here to avoid Vuex mutation errors
this.selectedProxy = { ...newVal }
this.$store.commit("SET_LEADER_REGIONS", { ...newVal })
},
},
And it works great now!
I think the issue is that you can't edit a v-model value directly, and also you also have to be aware of passing references to objects, and so the object spread operator is a real help.

Vuex: Add Dynamic Property to Object without triggering Watchers on existing Properties

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.

VueJS - Dynamic State Management multiple instances

I am creating an app and I have a component "Message" which uses a store to get data back from a JSON file (this will be eventually a database) and the component is as follows:
export default {
props: ['message'],
mounted: function() {
this.$store.dispatch("FETCHMESSAGE", this.message);
},
computed: {
title: function() {
return this.$store.state.message;
}
}
}
I have the following mutation:
FETCHMESSAGE: function (context, type)
{
var data = json.type; // Get the data depending on the type passed in
// COMMIT THE DATA INTO THE STORE
}
And I use it as the following:
<MessageApp message="welcome"></MessageApp>
This works for the most part and the correct message is displayed. The issue is when I have multiple instances of MessageApp being called on the same page. They both show the same message (of the last message) being called. E.g.
<MessageApp message="welcome"></MessageApp>
<MessageApp message="goodbye"></MessageApp>
They will each show the goodbye message. I know why this is happening but is it possible to have multiple instances of the store so that this does not happen?
Vuex is "a centralized store for all the components in an application," as the docs say.
So imagine that you have a variable (or many) which you can use and change from all your components.
Also when you want to get properties from state, it is recommended to use getters.
I can't understand what you want to do, but if you want, you can have multiple states, getters, mutations and actions and use them as modules in the store (read more). See below example from Vuex docs:
const moduleA = {
state: { title: '' },
mutations: { changeTitle(state, payload) { state.title = payload } },
actions: { changeTitle({commit}, payload) { commit('changeTitle', payload) } },
getters: { getTitle(state) { return state.title } }
}
const moduleB = {
state: { title: '' },
mutations: { changeTitle(state, payload) { state.title = payload } },
actions: { changeTitle({commit}, payload) { commit('changeTitle', payload) } },
getters: { getTitle(state) { return state.title } }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

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.