Wait for data in mapstate to finish loading

Viewed 635

I have stored a userProfile in Vuex to be able to access it in my whole project. But if I want to use it in the created() hook, the profile is not loaded yet. The object exists, but has no data stored in it. At least at the initial load of the page. If I access it later (eg by clicking on a button) everything works perfectly. Is there a way to wait for the data to be finished loading?

Here is how userProfile is set in Vuex:

mutations: {
    setUserProfile(state, val){
      state.userProfile = val
    }
},
actions: {
    async fetchUserProfile({ commit }, user) {
      // fetch user profile
      const userProfile = await fb.teachersCollection.doc(user.uid).get()
  
      // set user profile in state
      commit('setUserProfile', userProfile.data())
    },
}

Here is the code where I want to acess it:

<template>
<div>
  <h1>Test</h1>
  {{userProfile.firstname}}
  {{institute}}
</div>
</template>


<script>
import {mapState} from 'vuex';

export default {
  data() {
    return {
      institute: "",
    }
  },
  computed: {
      ...mapState(['userProfile']),
  },
  created(){
    this.getInstitute();
  },

  methods: {
    async getInstitute() {
      console.log(this.userProfile); //is here still empty at initial page load

      const institueDoc = await this.userProfile.institute.get();
      if (institueDoc.exists) {
        this.institute = institueDoc.name;
      } else {
        console.log('dosnt exists') 
      }
      
    }
  }
}
</script>

Through logging in the console, I found out that the problem is the order in which the code is run. First, the method getInstitute is run, then the action and then the mutation. I have tried to add a loaded parameter and played arround with await to fix this issue, but nothing has worked.

1 Answers

Even if you make created or mounted async, they won't delay your component from rendering. They will only delay the execution of the code placed after await.

If you don't want to render a portion (or all) of your template until userProfile has an id (or any other property your users have), simply use v-if

<template v-if="userProfile.id">
  <!-- your normal html here... -->
</template>
<template v-else>
   loading user profile...
</template>

To execute code when userProfile changes, you could place a watcher on one of its inner properties. In your case, this should work:

export default {
  data: () => ({
    institute: ''
  }),
  computed: {
    ...mapState(['userProfile']),
  },
  watch: {
    'userProfile.institute': {
      async handler(institute) {
        if (institute) {
          const { name } = await institute.get();
          if (name) {
            this.institute = name;
          }
        } 
      },
      immediate: true
    }
  }
}

Side note: Vue 3 comes with a built-in solution for this pattern, called Suspense. Unfortunately, it's only mentioned in a few places, it's not (yet) properly documented and there's a sign on it the API is likely to change.
But it's quite awesome, as the rendering condition can be completely decoupled from parent. It can be contained in the suspensible child. The only thing the child declares is: "I'm currently loading" or "I'm done loading". When all suspensibles are ready, the template default is rendered.
Also, if the children are dynamically generated and new ones are pushed, the parent suspense switches back to fallback (loading) template until the newly added children are loaded. This is done out of the box, all you need to do is declare mounted async in children.
In short, what you were expecting from Vue 2.

Related