Search code examples
vue.jsvuexvue-router

Execute Vuex action before initial routing


I have setup Vuex and Vue Router. I want to show a setup page, in case a users account isn't properly setup. This is implemented by making an API call to the backend. The result is stored inside Vuex and accessed using a getter. The action is dispatched in beforeCreate in the root Vuex instance:

new Vue({
  router,
  store,
  render: h => h(App),

  beforeCreate() {
    this.$store.dispatch("config/get");
  }
}).$mount("#app");

However, my router beforeEach never receives true but the getter is definitely returning true as per DevTools:

router.beforeEach((to, next) => {
  if (!to.matched.some(record => record.meta.requiresSetup)) {
    next();
    return;
  }

  if (!store.getters["config/isConfigured"]) { // Executed every time.
    next("/setup");
    return;
  }

  next();
});

Solution

  • Delaying app load

    It's not possible to use a lifecycle hook to delay loading even if the hook is marked async and performs an await for some async operation. Hooks aren't intended to allow manipulation, only to give access to the stages.

    Still, you can delay the app just by not loading it until the store action completes, but realize this means a blank page for the user, which is a bad user experience. But here's how you can do that:

    main.js

    const preload = async () => {
      await store.dispatch('config/get');    // `await` store action which returns a promise
      
      new Vue({
        store,
        router,
        render: (h) => h(App)
      }).$mount("#app");
    }
    
    preload();
    console.log('LOADING...');
    

    The better way

    It's better to dispatch the action and not wait for it. Let App.vue load and use v-if to show a splash screen until some store state isLoading is false:

    main.js

    store.dispatch('config/get');
    
    new Vue({
      store,
      router,
      render: (h) => h(App)
    }).$mount("#app");
    

    App.vue

    <template>
    <div>
      <div v-if="isLoading">
        LOADING...     <!-- Attractive splash screen here -->
      </div>
      <div v-else>
        <router-view></router-view>  <!-- Don't show the router view until ready -->
      </div>
    </div>
    </template>
    

    Remove the navigation guard completely and in App.vue, put a watch on isLoading. Once it's no longer loading, redirect based on the state of the account getter:

    computed: {
      ...mapState(['isLoading'])
    },
    methods: {
      redirect() {
        const path = this.$route.path;
        if (!this.$store.getters["config/isConfigured"]) {
          path !== '/setup' && this.$router.push({ path: '/setup' });
        } else {
          path !== '/' && this.$router.push({ path: '/' });
        }
      }
    },
    watch: {
      isLoading(val) {
        if (val === false) {
          this.redirect();
        }
      }
    }