Mutations must be synchronous. Using async code in mutations yield all kinds of untraceable weird behaviors.
As the docs suggest, you should move the asychronous code to an action, like:
mutations: {
[TYPES.FETCH_USER](state, data) {
state.user = data
}
//...
},
actions: {
fetchUsersActionName({ commit }) {
return api.get('user').then(({ data }) => {
commit(TYPES.FETCH_USER, data)
console.log(data)
}, (error) => {
console.log(error)
});
}
}
And use it with .dispatch(), not .commit(), like:
this.$store.dispatch('fetchUsersActionName');
The error
Check the below demos for reason why you get the error:
<h1>{{getUser.name}}</h1><!-- this works -->
<h2>{{getUser.number}}</h2><!-- this works -->
<h3>{{getUser.details.test}}</h3> <!-- THIS FAILS!!-->
This first demo throws an error, because initially getUser.details.test is undefined, and as such it tries to access undefined.test, which throws an error:
var globalUser = {
name:"test",
number:"9666-0503",
details:{
test:"cannot_access_this",
second_field:"nope_no_go"
}
};
var TYPES = {FETCH_USER: 'TYPESFETCHUSER'};
const store = new Vuex.Store({
strict: true,
state: {user: {}},
mutations: {
[TYPES.FETCH_USER](state, data) {
state.user = data
}
},
actions: {
fetchUsersActionName({ commit }) {
setTimeout(() => commit(TYPES.FETCH_USER, globalUser, 2000)); // simmulate asynchronous code
}
},
getters: {
getUser: state => state.user
}
});
new Vue({
store: store,
el: '#app',
computed: {
...Vuex.mapGetters(['getUser']),
},
mounted() {
this.getData()
},
methods: {
getData() { this.$store.dispatch('fetchUsersActionName'); }
}
})
span {font-family: monospace; font-weight: bold; color: red } h1,h2,h3 {font-size: medium; display: inline-block}
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<span>This throws an error, because initially .details is undefined.</span>
<div id="app">
<h1>{{getUser.name}}</h1> |
<h2>{{getUser.number}}</h2> |
<h3>{{getUser.details.test}}</h3>
</div>
Without .test there's no error:
var globalUser = {
name:"test",
number:"9666-0503",
details:{
test:"cannot_access_this",
second_field:"nope_no_go"
}
};
var TYPES = {FETCH_USER: 'TYPESFETCHUSER'};
const store = new Vuex.Store({
strict: true,
state: {user: {}},
mutations: {
[TYPES.FETCH_USER](state, data) {
state.user = data
}
},
actions: {
fetchUsersActionName({ commit }) {
setTimeout(() => commit(TYPES.FETCH_USER, globalUser, 2000)); // simmulate asynchronous code
}
},
getters: {
getUser: state => state.user
}
});
new Vue({
store: store,
el: '#app',
computed: {
...Vuex.mapGetters(['getUser']),
},
mounted() {
this.getData()
},
methods: {
getData() { this.$store.dispatch('fetchUsersActionName'); }
}
})
span {font-family: monospace; font-weight: bold; color: red } h1,h2,h3 {font-size: medium; display: inline-block}
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<span>This throws no error, and prints the whole details object.</span>
<div id="app">
<h1>{{getUser.name}}</h1> |
<h2>{{getUser.number}}</h2> |
<h3>{{getUser.details}}</h3>
</div>
Workaround: with .test but with v-if:
var globalUser = {
name:"test",
number:"9666-0503",
details:{
test:"cannot_access_this",
second_field:"nope_no_go"
}
};
var TYPES = {FETCH_USER: 'TYPESFETCHUSER'};
const store = new Vuex.Store({
strict: true,
state: {user: {}},
mutations: {
[TYPES.FETCH_USER](state, data) {
state.user = data
}
},
actions: {
fetchUsersActionName({ commit }) {
setTimeout(() => commit(TYPES.FETCH_USER, globalUser, 2000)); // simmulate asynchronous code
}
},
getters: {
getUser: state => state.user
}
});
new Vue({
store: store,
el: '#app',
computed: {
...Vuex.mapGetters(['getUser']),
},
mounted() {
this.getData()
},
methods: {
getData() { this.$store.dispatch('fetchUsersActionName'); }
}
})
span {font-family: monospace; font-weight: bold; color: red } h1,h2,h3 {font-size: medium; display: inline-block}
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<span>This throws no error, because the v-if controls the displaying to only when .test is available.</span>
<div id="app">
<h1>{{getUser.name}}</h1> |
<h2>{{getUser.number}}</h2> |
<h3 v-if="getUser.details">{{getUser.details.test}}</h3>
</div>
Workaround: with .test but with user initialized including a details property:
var globalUser = {
name:"test",
number:"9666-0503",
details:{
test:"cannot_access_this",
second_field:"nope_no_go"
}
};
var TYPES = {FETCH_USER: 'TYPESFETCHUSER'};
const store = new Vuex.Store({
strict: true,
state: {user: {details: {}}}, // <============ THIS IS THE IMPORTANT, notice the {details: {}}
mutations: {
[TYPES.FETCH_USER](state, data) {
state.user = data
}
},
actions: {
fetchUsersActionName({ commit }) {
setTimeout(() => commit(TYPES.FETCH_USER, globalUser, 2000)); // simmulate asynchronous code
}
},
getters: {
getUser: state => state.user
}
});
new Vue({
store: store,
el: '#app',
computed: {
...Vuex.mapGetters(['getUser']),
},
mounted() {
this.getData()
},
methods: {
getData() { this.$store.dispatch('fetchUsersActionName'); }
}
})
span {font-family: monospace; font-weight: bold; color: red } h1,h2,h3 {font-size: medium; display: inline-block}
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<span>This throws no error, because the user initial value contains a "details" field already.</span>
<div id="app">
<h1>{{getUser.name}}</h1> |
<h2>{{getUser.number}}</h2> |
<h3>{{getUser.details.test}}</h3>
</div>
Why?
This is because the code that updates user is asynchronous.
First time it tries to display {{ getUser.name }}, the getUser.name value is undefined so it just shows nothing (because Vue shows nothing when {{ undefined }}). Millisseconds later, getUser.name is updated and then begins to show the name. It's very fast, which is why you get the sensation that it never was undefined.
The same happens to {{ getUser.number }} and {{ getUser.details }}.
When it tries to display {{ getUser.details.test }}, on the other hand, it will throw an error because, at first, getUser.details is undefined so when it tries to access getUser.details.test it is effectively the same as undefined.test, which is why it throws TypeError: Cannot read property 'test' of undefined.
For workarounds, either use v-if or initialize user with a non-undefined details property (see last two demos above).
this.$store.dispatch(TYPES.FETCH_USER);which would call theTYPES.FETCH_USERaction, not the mutation. Also, a mutation just has thestateobject as the first parameter. You have{ state }, which is a destructured object containing with astateproperty. So, if you are actually somehow calling the mutation, you're settingstate.state.user, which isn't what you wantgetUseris going to be set to whatever the value ofstate.useris initialized to in your vuex store. It seems most likely that the initial value ofstate.useris an empty object, so trying to accessstate.user.details.testresults in an error. The simplest fix would be to initializestate.userwith adetailsproperty:state: { user: { details: {} } }