26

For such component

<template>
  <div>
    <router-link :to="{name:'section', params: { sectionId: firstSectionId }}">Start</router-link>
  </div>
</template>
    
<script lang="ts">
  import { mapActions } from "vuex"
    
  export default {
    mounted() {
      this.getSectionId()
    },
    computed: {
      firstSectionId() {
        return this.$store.state.firstSectionId
      }
    },
    methods: mapActions(["getSectionId"])
  }
</script>

Store:

const store: any = new Vuex.Store({
    state: {
        firstSectionId: null
    },
    // actions,
    // mutations
})

I have a web request in the getSectionId action and it asynchronously fetches data and calls a mutation that will fill firstSectionId in state. During the initial rendering firstSectionId is null and I get the warning that a required parameter is missing during rendering of router-link.

It is not a problem here to add v-if="firstSectionId". But in general what is the approach for fetching data from a server to be displayed? Currently all my components are checking if there is data present in the store before rendering, is it normal or is there a better way to wait for data to be loaded before rendering it?

André Kuhlmann
  • 4,378
  • 3
  • 23
  • 42
Sly
  • 15,046
  • 12
  • 60
  • 89
  • 1
    To *wait for data load before rendering it*, you use server-side rendering. Apart from that, why "wait for data"? We see websites and apps every day which load static page structures with "loading" indicators, and fill them with data later when they arrive. Obviously, showing the user as much as we can, as early as possible presents best expierence. – JJPandari Jan 13 '17 at 09:05

3 Answers3

65

One approach for asynchronously fetching data is to use promise in vuex store actions.

Vue.http.get(API_URL)
  .then((response) => {
     //use response object      
  })
  .catch((error) => {
    console.log(error.statusText)
  });

To demonstrate that I make request to this route. You can see how response should looks like. Let's save response object in state.users array.

store.js

const store = new Vuex.Store({
  state: {
    users: []
  },  
  mutations: {
    FETCH_USERS(state, users) {
      state.users = users
    }
  },
  actions: {
    fetchUsers({ commit }, { self }) {          
      Vue.http.get("https://jsonplaceholder.typicode.com/users")
        .then((response) => {
          commit("FETCH_USERS", response.body);
          self.filterUsers();   
        })
        .catch((error) => {
          console.log(error.statusText)
        });
    }
  }
})
    
export default store

You noticed that there is self.filteruser() method after commit. That is crucial moment. Before that we are committing a mutation, which is synchronous operation and we are sure that we will have our response in store.state that can be used in filterUsers() method (don't forget to pass self parm)

Users.vue

import store from "../store/store"

export default {
  name: 'users',
  created() {
    this.$store.dispatch("fetchUsers", { self: this })       
  },
  methods:{
    filterUsers() {
      //do something with users
      console.log("Users--->",this.$store.state.users)       
    }
  }
}

Better ways (ES6 & ES7)

ES6 Promises for asynchronous programming

//User.vue
created() {
  this.$store.dispatch("fetchUser").then(() => {
    console.log("This would be printed after dispatch!!")
  })
}

//store.js
actions: {
  fetchUser({ commit }) {
    return new Promise((resolve, reject) => {
      Vue.http.get("https://jsonplaceholder.typicode.com/users")
        .then((response) => {
          commit("FETCH_USERS", response.body);
          resolve();
         })
         .catch((error) => {
           console.log(error.statusText);
         });
    });
  }
}

ES7: async/await

To get away from callback hell, and to improve asynchronous programming use async function, and you can await on a promise. Code looks much easier to follow (like it is synchronous), but code isn't readable for browsers so you'll need Babel transpiler to run it.

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // wait for actionA to finish
    commit('gotOtherData', await getOtherData())
  }
}
André Kuhlmann
  • 4,378
  • 3
  • 23
  • 42
t_dom93
  • 10,226
  • 1
  • 52
  • 38
  • Nothing works for me. I am trying to fetch content from WordPress API but page loads in about 40ms and API call is being sent after page load. I am using Server Side Rendering and want to render the content on page load. Vuex stores doesn't help – Shahbaz Ahmed May 29 '18 at 15:01
  • The ES6 way helped me. – MCFreddie777 Jan 25 '20 at 20:20
3

In my experience, you can skip a few checks if you preset the state with an empty value of the same type as the expected result (if you know what to expect, of course), e.g. if you have an array of items, start with [] instead of null as it won't break v-for directives, .length checks and similar data access attempts.

But generally, adding v-if is a very normal thing to do. There's a section about this in the vue-router documentation and checking whether properties exist or not is exactly what it suggests. Another possible solution it mentions is fetching data inside beforeRouteEnter guard, which assures you will always get to the component with your data already available.

Ultimately, both solutions are correct, and the decision between them is more of a UX/UI question.

mzgajner
  • 2,250
  • 1
  • 17
  • 14
0

I had similar requirements for locations and the google map api. I needed to fetch my locations from the API, load them in a list, and then use those in a map component to create the markers. I fetched the data in a Vuex action with axios, loaded that in my state with a mutation, and then used a getter to retrieve the resulting array in the mounted life cycle hook. This resulted in an empty array as mounted fired before the async action resolved.

I used store.subscribe to solve it this way:

<template>
  <div class="google-map" :id="mapName"></div>
</template>

<script>
import GoogleMapsLoader from 'google-maps';
import { mapGetters } from 'vuex';

export default {
  name: 'google-map',
  props: ['name'],
  computed: {
    ...mapGetters({
      locations: 'locations/locations',
    }),
  },
  data() {
    return {
      mapName: `${this.name}-map`,
    };
  },
  mounted() {
    this.$store.subscribe((mutation, state) => {      
      if (mutation.type === 'locations/SAVE_LOCATIONS') {
        GoogleMapsLoader.KEY = 'myKey';
        GoogleMapsLoader.load((google) => {
          /* eslint-disable no-new */
          const map = new google.maps.Map(document.getElementById('locations-map'));

          // loop through locations and add markers to map and set map boundaries
          const bounds = new google.maps.LatLngBounds();

          // I access the resulting locations array via state.module.property
          state.locations.locations.forEach((location) => {
            new google.maps.Marker({
              position: {
                lat: location.latitude,
                lng: location.longitude,
              },
              map,
            });
            bounds.extend({
              lat: location.latitude,
              lng: location.longitude,
            });
          });

          map.fitBounds(bounds);
        });
      }
    });
  },
};
Nathan Agersea
  • 496
  • 3
  • 17
  • Thank you this really helped me out. Kept having a blank store on initial page load and using a computed value did not help. Only $store.subscribe worked which I don't understand *why* but I'm grateful. Do you happen to know why it works better over computed? – volume one Jun 06 '21 at 21:50
  • This was from several years ago and, looking at it now, I should have fetched the data that drives the markers before loading the map. That would have been much cleaner. – Nathan Agersea Jun 14 '21 at 23:14