Wait for VueX value to load, before loading component

Viewed 16563

When a user tries to directly navigate load a component url, an http call is made in my vuex actions, which will define a value in my state once it resolves.

I don't want to load my component until the http call is resolved, and the state value is defined.

For Example, in my component

export default {
  computed: {
    ...mapState({
      // ** this value needs to load before component mounted() runs **
      asyncListValues: state => state.asyncListValues
    })
  },

  mounted () {
    // ** I need asyncListValues to be defined before this runs **
    this.asyncListValues.forEach((val) => { 
      // do stuff
    });
  }
}

How can I make my component wait for asyncListValues to load, before loading my component?

5 Answers

One way to do it is to store state values.

For example, if your store relies on single API, you would do something like this. However, for multiple APIs, it's a good idea to store each api load state individually, or using a dedicated object for each API.

There are usualy 4 states that you can have, which I prefer to have in a globally accessible module:

// enums.js
export default {
  INIT: 0,
  LOADING: 1,
  ERROR: 2,
  LOADED: 3
};

Then, you can have the variable stored in the vuex state, where the apiState is initialized with INIT. you can also initialize the array with [], but that shouldn't be necessary.

import ENUM from "@/enums";
// store.js
export default new Vuex.Store({
  state: {
    apiState: ENUM.INIT,
    accounts: [],
    // ...other state
  },
  mutations: {
    updateAccounts (state, accounts) {
      state.accounts = accounts;
      state.apiState = ENUM.LOADED;
    },
    setApiState (state, apiState) {
      state.apiState = apiState;
    },
  },
  actions: {
    loadAccounts ({commit) {
      commit('setApiState', ENUM.LOADING);
      someFetchInterface()
        .then(data=>commit('updateAccounts', data))
        .catch(err=>commit('setApiState', ENUM.ERROR))
    }
  }
});

Then, by adding some computed variables, you can toggle which component is shown. The benefit of using state is that you can easily identify the Error state, and show a loading animation when state is not ready.

<template>
  <ChildComponent v-if="apiStateLoaded"/>
  <Loader v-if="apiStateLoading"/>
  <Error v-if="apiStateError"/>
</template>
<script>
import ENUM from "@/enums";
export default {
  computed: {
    ...mapState({
      apiState: state=> state.apiState
    }),
    apiStateLoaded() {
      return this.apiState === ENUM.LOADED;
    },
    apiStateLoading() {
      return this.apiState === ENUM.LOADING || this.apiState === ENUM.INIT;
    },
    apiStateError() {
      return this.apiState === ENUM.ERROR;
    },
  })
}
</script>

aside... I use this pattern to manage my applications as a state machine. While this example utilizes vuex, it can be adapted to use in a component, using Vue.observable (vue2.6+) or ref (vue3).

Alternatively, if you just initialize your asyncListValues in the store with an empty array [], you can avoid errors that expect an array.

Since you mentioned vue-router in your question, you can use beforeRouteEnter which is made to defer the rendering of a component.

For example, if you have a route called "photo":

import Photo from "../page/Photo.vue";

new VueRouter({
  mode: "history",
  routes: [
    { name: "home", path: "/", component: Home },
    { name: "photo", path: "/photo", component: Photo }
  ]
});

You can use the beforeRouteEnter like this:

<template>
  <div>
    Photo rendered here
  </div>
</template>
<script>
export default {
  beforeRouteEnter: async function(to, from, next) {
    try {
      await this.$store.dispatch("longRuningHttpCall");

      next();
    } catch(exception) {
      next(exception);
    }
  }
}
</script>

What it does is, waiting for the action to finish, updating your state like you want, and then the call to next() will tell the router to continue the process (rendering the component inside the <router-view></router-view>).

Tell me if you need an ES6-less example (if you do not use this syntax for example).

You can check the official documentation of beforeRouteEnter on this page, you will also discover you can also put it at the route level using beforeEnter.

One approach would be to split your component into two different components. Your new parent component could handle fetching the data and rendering the child component once the data is ready.

ParentComponent.vue

<template>
  <child-component v-if="asyncListValues && asyncListValues.length" :asyncListValues="asyncListValues"/>
  <div v-else>Placeholder</div>
</template>
<script>
export default {
  computed: {
    ...mapState({
      asyncListValues: state => state.asyncListValues
    })
  }
}
</script>

ChildComponent.vue

export default {
  props: ["asyncListValues"],
  mounted () {
    this.asyncListValues.forEach((val) => { 
      // do stuff
    });
  }
}

Simple way for me:

...
watch: {
   vuexvalue(newVal) {
      if (newVal == 'XXXX')
        this.loadData()
      }
   }
},
computed: {
   ...mapGetters(['vuexvalue'])
}

Building on some of the other answers, if you're using Router, you can solve the problem by only calling RouterView when the state has been loaded.

Start with @daniel's approach of setting a stateLoaded flag when the state has been loaded. I'll just keep it simple here with a two-state flag, but you can elaborate as you like:

const store = createStore({
  state () {
    return {
      mysettings: {},         // whatever state you need
      stateLoaded: false,
    }
  },
  mutations: {
    set_state (state, new_settings) {
      state.settings = new_settings;
      state.stateLoaded = true;
    },
  }
}

Then, in app.vue you'll have something like this:

<div class="content">
  <RouterView/>
</div>

Change this to:

<div class="content">
  <RouterView v-if="this.$store.state.stateLoaded"/>
</div>

The v-if won't even attempt to do anything with RouterView until the (reactive) stateLoaded flag goes true. Therefore, anything you're rendering with the Router won't get called, and so there won't be any undefined state variables in it when it does get loaded.

You can of course build on this with a v-else to perhaps show a "Loading..." screen or something, just in case the state loading takes longer than expected. Using @daniel's multi-state flag, you could even report if there was a problem loading the state, and offer a Retry button or something.

Related