0

I have an object

   var user = {
      name:"test",
      number:"9666-0503",
      details:{
          test:"cannot_access_this",
          second_field:"nope_no_go"
      }
   }

Im calling an action in Vue JS

[TYPES.FETCH_USER]({ commit }) {
    api.get('user').then(({ data }) => {
        commit(TYPES.STORE_USER, data)
        console.log(data)
        // This returns perfectly fine and valid.  If I do 
        //console.log(data.details.test), I get the correect value
    }, (error) => {
        console.log(error)
    })
},

Then the mutation

[TYPES.STORE_USER](state, data) {
    state.user = data
    localStorage.set("user", state.user)
},

In my getters file

getUser: state => {
    return state.user
}

In my Component Im doing

computed: {
  ...mapGetters(["getUser"]),
},
mounted(){
   this.getData()
},
methods: {
  getData() {
    this.$store.dispatch(TYPES.FETCH_USER);
  }

}

And in the template

    <h1>{{getUser.name}}</h1><!-- this works -->
    <h2>{{getUser.number}}</h2><!-- this works -->
    <h3>{{getUser.details.test}}</h3> <!-- THIS FAILS!!-->

So getUser.details.test fails. However...if I just do

    <h3>{{getUser.details}}</h3>

It then prints out what appears to be a string version of the details object??! Like what follows....

    <h3>{"test":"cannot_access_this","second_field":"nope_no_go"}</h3>

What is going on here!??

The wierdest thing is if I just console.log(state.user) at any time, it returns correctly. And is a full object. Why when Im accessing it in a template, is it stringifying any nested objects?

3
  • You're calling this.$store.dispatch(TYPES.FETCH_USER); which would call the TYPES.FETCH_USER action, not the mutation. Also, a mutation just has the state object as the first parameter. You have { state }, which is a destructured object containing with a state property. So, if you are actually somehow calling the mutation, you're setting state.state.user, which isn't what you want Commented Mar 15, 2018 at 4:25
  • No sorry I used the wrong wording, Im calling an action. the action is TYPES.FETCH_USER. Ive changed my code to reflect the answer below. Still doesn work however.... :( It only seems to work for single level objects. Any nesting returns stringed versions of these objects Commented Mar 15, 2018 at 4:30
  • So yeah, like acdcjunior said, initially, getUser is going to be set to whatever the value of state.user is initialized to in your vuex store. It seems most likely that the initial value of state.user is an empty object, so trying to access state.user.details.test results in an error. The simplest fix would be to initialize state.user with a details property: state: { user: { details: {} } } Commented Mar 15, 2018 at 4:37

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

11 Comments

I'm sorry, I'm don't follow, the template stringifies objects to show them. Is that the issue?
No, its stringifying the whole object. Displaying that. But if I try to acecss those properties, it says undefined. So {{user.details.test}} is undefined. But {{user.details}} echoes out '{"test":"data","second_field":"more_data"}' as a string. But I cant access the test or second_field as objects. Theyre in that details string, as a string, instead of an object. Its only nested objects that this is happening on. Returning objects with no nesting, I can access them perfectly fine
It is because the code is asynchronous. First time it tries to load, getUser.name is undefined so it just shows nothing. Millisseconds later, getUser.name is updated and then begins to thow the name. The same happens to getUser.number and getUser.details. OTOH, when you put {{ getUser.details.test }}, it will throw an error because, at first, getUser.details is undefined and undefined.test is an error.
yeah, but why is it outputting a string? Its not undefined. Its a string
Have a look at the demos, specially the last, with v-if.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.