58
votes

I am using Vue.js 2.0 and Vuex 2.0 for a small app. I am initializing the the store in the 'created' life-cycle hook on the root Vue instance by calling an action that retrieves the initial state from an API....like so in my Root Component:

const app = new Vue({
 el: "#app",
 router,
 store,
 data: {
     vacation: {},
 },
 components: {
    'vacation-status': VacationStatus,
 },
 created(){
    //initialize store data structure by submitting action.
    this.$store.dispatch('getVacation');
 },
 computed: {
 },
 methods: {
 }
});

This is working just fine. Here is the action on my store that I'm calling here:

getVacation({commit}){
  api.getVacation().then(vacation => commit(UPDATE_VACATION, vacation))
}

The mutation that this is committing with 'UPDATE_VACATION' is here:

[UPDATE_VACATION] (state, payload) {
  state.vacation = payload.vacation;
},

My Problem: When I load the app, all my components that are 'getting' values from the store throw errors I'm trying to access 'undefined' values on the store. In other words, state hasn't been initialized yet.

For example, I have a component that has getters in Child Components like this:

computed: {
        arrival () {
            return this.$store.getters.arrival
        },
        departure() {
            return this.$store.getters.departure
        },
        countdown: function() {
            return this.$store.getters.countdown
        }
} 

All these getters cause errors because 'vacation' is undefined on the state object. It seems like an asynchronous problem to me, but could be wrong. Am I initializing my store state in the wrong spot?

  Vue.use(Vuex);

  export default new Vuex.Store({
    state: {},
    getters: {
    getVacation: state => {
        return state.vacation
    },
    guests: state => {
        return state.vacation.guests
    },
    verifiedGuests: state => {
        return state.vacation.guests.filter(guest => guest.verified)
    },
    emergencyContacts: state => {
        return state.emergency_contacts
    },
    arrival: state => {
        return state.vacation.check_in
    },
    departure: state => {
        return state.vacation.check_out
    },
    countdown : state => {
        let check_in = new Date(state.vacation.check_in);
        let now = new Date();

        if ((now - check_in) > 0) {
            return 'This vacation started on ' + check_in;
        }

        let difference = check_in - now;
        let day = 1000 * 60 * 60 * 24;

        return Math.ceil(difference / day) + " days until your vacation";
    }
},
mutations: {
    [UPDATE_VACATION] (state, payload) {
        state.vacation = payload.vacation;
    },
    [ADD_GUEST] (state, payload) {
        state.vacation.guests.push(payload.guest);
    },
    [REMOVE_GUEST] (state, payload){
        state.vacation.guests.filter(guest => { debugger; return guest.id != payload.guest.id})
    },
    [UPDATE_GUEST] (state, payload){
        state.vacation.guests.map(guest => {
            // Refactor Object.assign to deep cloning of object
            return guest.id === payload.guest.id ? Object.assign({}, guest, payload.guest) : guest;
        })
    },
    [ADD_EMERGENCY] (state, payload){
        state.vacation.emergency_contacts.push(payload.emergency_contact)
    },
    [REMOVE_EMERGENCY] (state, payload){
        state.vacation.emergency_contacts.filter(contact => contact.id !== payload.emergency_contact.id)
    },
    [UPDATE_EMERGENCY] (state, payload){
        state.vacation.emergency_contacts.map(contact => {
            // Refactor not needed because emergency_contact is a shallow object.
           return contact.id === payload.emergency_contact.id ? Object.assign({}, contact, payload.emergency_contact) : contact;
        });
    }
},
actions: {
    getVacation({commit}){
      api.getVacation().then(vacation => commit(UPDATE_VACATION, vacation))
    },
    addGuest({commit}, guest){
        commit(ADD_GUEST, guest);
    },
    removeGuest({commit}, guest){
        commit(REMOVE_GUEST, guest);
    },
    updateGuest({commit}, guest){
        commit(UPDATE_GUEST, guest);
    },
    addEmergency({commit}, guest){
        commit(ADD_EMERGENCY, contact)
    },
    removeEmergency({commit}, contact){
        commit(REMOVE_EMERGENCY, contact)
    },
    updateEmergency({commit}, contact){
        commit(UPDATE_EMERGENCY, contact)
    },
    updateServer(store, payload){
      return api.saveVacation(payload)
    }
}

});

Just so the solution is clear to others:

I wasn't setting my inital state quite properly in the store itself. I was pulling in the data, and updating the store correctly, but the store needed to be initialized like this:

export default new Vuex.Store({
 state: {
     vacation: {}//I added this, and then justed updated this object on create of the root Vue Instance
 },
});
2

2 Answers

40
votes

I think you're doing everything right. Maybe you're just not creating the getters correctly (can't see any definition in your code). Or your setting the initial state not correctly (also not visible in your snippet).

I would use mapState to have the state properties available in components.

In the demo simply add users to the array in mapState method parameter and the users data will be available at the component. (I've just added the getter users to show how this is working. That's not needed if you're using mapState.)

Please have a look at the demo below or this fiddle.

const api =
  'https://jsonplaceholder.typicode.com/users'

const UPDATE_USERS = 'UPDATE_USERS'
const SET_LOADING = 'SET_LOADING'

const store = new Vuex.Store({
  state: {
    users: {},
    loading: false
  },
  mutations: {
    [UPDATE_USERS](state, users) {
      console.log('mutate users', users)
      state.users = users;
      console.log(state)
    }, [SET_LOADING](state, loading) {
      state.loading = loading;
    }
  },
  getters: {
    users(state) {
      return state.users
    }
  },
  actions: {
    getUsers({commit}) {
      commit(SET_LOADING, true);
      return fetchJsonp(api)
        .then((users) => users.json())
        .then((usersParsed) => {
          commit(UPDATE_USERS, usersParsed)
          commit(SET_LOADING, false)
        })
    }
  }
})

const mapState = Vuex.mapState;

const Users = {
  template: '<div><ul><li v-for="user in users">{{user.name}}</li></ul></div>',
  computed: mapState(['users'])
}

new Vue({
  el: '#app',
  store: store,
  computed: {
    ...mapState(['loading']),
      //...mapState(['users']),
      /*users () { // same as mapState
      	return this.$store.state.users;
      }*/
      users() { // also possible with mapGetters(['users'])
        return this.$store.getters.users
      }
  },
  created() {
    this.$store.dispatch('getUsers')
  },
  components: {
    Users
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch-jsonp/1.0.5/fetch-jsonp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.1.1/vuex.min.js"></script>
<div id="app">
  <div v-if="loading">loading...</div>
  <users></users>
  <pre v-if="!loading">{{users}}</pre>
</div>
-2
votes

You can create a function that returns the initial state, and use it into your Vuex instance, like this:

function initialStateFromLocalStorage() {
    ...
    const empty = {
        status: '',
        token: '',
        user: null
    }
    return empty;
}

export default new Vuex.Store({
    state: initialStateFromLocalStorage,
    ...

As soon as you return an object for the state, you can do whatever you want inside that function, right?