Wait for VueX value to load, before loading component
Asked Answered
G

5

7

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?

Gadgeteer answered 13/6, 2019 at 19:59 Comment(0)
D
16

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.

Dissolute answered 13/6, 2019 at 21:24 Comment(3)
I think this is probably the best solution. But if anyone is reading this, there are some other good solutions recommended hereGadgeteer
Yes, I like this idea too... I use the router method below but can think of use cases for this solution...Rigsby
wonderfull idea.i'll start adding this structure to all my apps.thank youBooma
N
5

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.

Niersteiner answered 13/6, 2019 at 21:23 Comment(1)
'The beforeRouteEnter guard does NOT have access to this, because the guard is called before the navigation is confirmed, thus the new entering component has not even been created yet.' - router.vuejs.org/guide/advanced/…Orangy
C
2

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
    });
  }
}
Cocklebur answered 13/6, 2019 at 21:12 Comment(1)
this seems like it would work. I might try this if I can't find anything else. Thanks!Gadgeteer
H
2

Simple way for me:

...
watch: {
   vuexvalue(newVal) {
      if (newVal == 'XXXX')
        this.loadData()
      }
   }
},
computed: {
   ...mapGetters(['vuexvalue'])
}
Herwick answered 30/5, 2020 at 9:7 Comment(0)
P
0

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.

Parrot answered 24/2, 2022 at 10:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.