Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make Vue Router Guards wait for Vuex?

So all answers I've found publicly to this question weren't very helpful and while they "worked", they were incredibly hacky.

Basically I have a vuex variable, appLoading which is initially true but gets set to false once all async operations are complete. I also have another vuex variable called user which contains user information that gets dispatched from the async operation once it gets returned.

I then also have a router guard that checks;

router.beforeEach((to, from, next) => {
  if (to.matched.some(route => route.meta.requiresAuth)) {
    if (store.getters.getUser) {
      return next();
    }
    return router.push({ name: 'index.signup' });
  }

  return next();
});

In my initial Vue instance I then display a loading state until appLoading = false;

Now this "works" but there is a problem which is really bugging me. If you load the page, you will get a "flicker" of the opposite page you are supposed to see.

So if you are logged in, on first load you will see a flicker of the signup page. If you aren't logged in, you will see a flicker of the logged in page.

This is pretty annoying and I narrowed the problem down to my auth guard. Seems it's pushing the signup page to the router since user doesn't exist then instantly pushes to the logged in page since user gets committed.

How can I work around this in a way that isn't hacky since it's kinda annoying and it's sort of frustrating that Vue doesn't have even the slightest bit of official docs/examples for a problem as common as this, especially since such a large number of webapps use authentication.

Hopefully someone can provide some help. :)


2 Answers

The router beforeEach hook can be a promise and await for a Vuex action to finish. Something like:

router.beforeEach(async (to, from, next) => {
  if (to.matched.some(route => route.meta.requiresAuth)) {
    await store.dispatch('init');

    if (store.getters.getUser) {
      return next();
    }
    return router.push({ name: 'index.signup' });
  }

  return next();
});

The 'init' action should return a promise:

const actions = {
  async init({commit}) {
    const user = await service.getUser();
    commit('setUser', user);
  }
}

This approach has the problem that whenever we navigate to a given page it will trigger the 'init' action which will fetch the user from the server. We only want to fetch the user in case we don't have it, so we can update the store check if it has the user and fetch it acordingly:

const state = {
  user: null
}
const actions = {
  async init({commit, state}) {
    if(!state.user) {
      const user = await service.getUser();
      commit('setUser', user);
    }
  }
}
like image 104
golfadas Avatar answered Sep 05 '25 00:09

golfadas


As per discussion in comments:

Best approach for you case will be if you make your appLoading variable a promise. That's how you can do things or wait for things until your app data is resolved.

Considering appLoading a promise which you initialize with your api call promise, your router hook will be like:

router.beforeEach((to, from, next) => {
  appLoading.then(() => {
    if (to.matched.some(route => route.meta.requiresAuth)) {
      if (store.getters.getUser) {
        return next();
      }
      return router.push({ name: "index.signup" });
    }

    return next();
  });
});

You might want to just keep it as an export from your init code instead of keeping it in Vuex. Vuex is meant for reactive data that is shared over components.

like image 37
pranavjindal999 Avatar answered Sep 05 '25 00:09

pranavjindal999



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!