How to work with async/await in vue-router? - vue.js

In vue router beforeEach I have code:
function foo() {
return new Promise(resolve => {
//load data from http request
resolve()
})
}
router.beforeEach(async (to, from, next) => {
await foo()
next()
}
The problem is only when I refresh the page or enter first time, beforeach is calling two times, I don't know why? How can I override this behaviour? I want to wait some data from API before user enters the page.
If there is no async await in beforeEach it works as expected.

Related

Calling methods inside beforeRouteEnter NProgress

I am trying to call an API before navigating to the route. The problem is that if I try to call axios call inside beforeRouteEnter it is working fine for example:
{
beforeRouteEnter(routeTo, routeFrom, next) {
NProgress.start()
axios.get('https://jsonplaceholder.typicode.com/posts').then((res) => {
next((vm) => {
vm.data = res.data
})
NProgress.done()
})
},
}
But when I try to call an API from methods it's navigating to the route before resolving an API and also NProgress bar is also completing before resolving a call.
{
beforeRouteEnter(routeTo, routeFrom, next) {
NProgress.start()
next((vm) => {
vm.index()
NProgress.done()
})
},
methods: {
index() {
axios
.get('https://jsonplaceholder.typicode.com/posts')
.then((res) => {
console.log(res.data)
})
.catch((error) => {
console.log(error)
})
},
},
}
Can anyone guide me what may be wrong?
In your first example, you set the progress bar, then you call the API with Axios and with .then you chain a function after the call. This means the function will wait until the promise is resolved or rejected, before continuing. Only when the axios call is finished successfully, the next function is executed in which you set the data and stop the progress bar. You also should use .catch for if the promises rejects.
Now in your second example, you do not use promises in beforeRouteEnter. Which basically means that all lines are executed immediately. So you call vm.index() and without waiting for the axios call to finish the next line, NProgress.done() will be executed. Although there are several ways to solve this my preference is use async/await, which is just a cleaner way to use promisses and chaining.
In your case I think this would work:
beforeRouteEnter(routeTo, routeFrom, next) {
NProgress.start();
await vm.index();
NProgress.done();
next();
});
}
And the method:
methods: {
async index () {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
console.log(response);
} catch (error) {
console.log(error);
}
}
}
Since this is only a part of your component I cannot test it, but I think you get the idea.

Vue3 - Using beforeRouteEnter to prevent flashing content

I am using Vue with axios like
[...]
import VueAxios from "vue-axios";
import axios from "#/axios";
createApp(App)
.use(store)
.use(router)
.use(VueAxios, axios)
.mount("#app");
[...]
which works really great globally like this.axios... everywhere. I am also using Passport for authentification and in my protected route I would like to call my Express-endpoint .../api/is-authenticated to check if the user is logged in or not.
To make this work I would like to use the beforeRouteEnter-navigation guard, but unfortunately I can't call this in there.
Right now I am having in in the mounted-hook, which feels wrong. Is there any solution with keeping my code straight and clean?
I'd appreciate a hint. Thanks.
Edit: This worked for me.
beforeRouteEnter(to, from, next) {
next((vm) => {
var self = vm;
self
.axios({ method: "get", url: "authenticate" })
.then() //nothing needed here to continue?
.catch((error) => {
switch (error.response.status) {
case 401: {
return { name: "Authentification" }; //redirect
//break;
}
default: {
return false; //abort navigation
//break;
}
}
});
});
With beforeRouteEnter there is access to the component instance by passing a callback to next. So instead of this.axios, use the following:
beforeRouteEnter (to, from, next) {
next(vm => {
console.log(vm.axios); // `vm` is the instance
})
}
Here's a pseudo-example with an async request. I prefer async/await syntax but this will make it clearer what's happening:
beforeRouteEnter(to, from, next) {
const url = 'https://jsonplaceholder.typicode.com/posts';
// ✅ Routing has not changed at all yet, still looking at last view
axios.get(url).then(res => {
// ✅ async request complete, still haven't changed views
// Now test the result in some way
if (res.data.length > 10) {
// Passed the test. Carry on with routing
next(vm => {
vm.myData = res.data; // Setting a property before the component loads
})
} else {
// Didn't like the result, redirect
next('/')
}
})
}

Prevent route change on refresh with Vue navigation guards

I have navigation guards working perfectly based on a userTp value, however whenever the user reloads a page it redirects them to name: 'login'.
I would like to prevent a route change on refresh. Is there a simple way to do this within navigation guards?
router.beforeEach(async (to, from, next) => {
if (to.meta.requireAuth) {
if (!store.state.auth.user) {
await store.dispatch(AUTH_REFRESH)
}
if (to.meta.userTp === store.state.auth.user.userTp) {
next()
window.scrollTo(0, 0)
} else {
next({ name: 'login' })
window.scrollTo(0, 0)
}
} else {
next()
window.scrollTo(0, 0)
}
})
EDIT:
I found a semi-workaround for this, but it's not perfect. It allows users to access pages if they know the correct page url path, but since my setup has a server-side userTp check before sending data it just shows the page template but without any data in it.
My temp solution:
(Added as the first if statement)
router.beforeEach(async (to, from, next) => {
if (to = from) {
if (!store.state.auth.user) {
await store.dispatch(AUTH_REFRESH)
to()
}
}

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. :)
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);
}
}
}
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.

How to redirect to another router from an AuthorizedStep

I have two routes — one is a public router and the other is the authorized router.
In my authorized router I have an authorizeStep.
The authorizedStep checks if there is a token in localStorage and if it is returns a next().
However if it fails to find a token its supposed to stop and jump out returning to the public router.
I am having trouble stopping the authorized step and instead going to the public router.
I have:
run(navigationInstruction: NavigationInstruction, next: Next): Promise<any> {
return Promise.resolve()
.then(() => this.checkSessionExists(navigationInstruction, next)
.then(result => result || next())
);
}
checkSessionExists(navigationInstruction: NavigationInstruction, next: Next) {
const session = this.authService.getIdentity();
if (!session) {
// HOW DO I CANCEL THE NEXT HERE AND GO TO THE PUBLIC ROUTER?
return next.cancel(new Redirect('login'))
}
return next()
}
forceReturnToPublic() {
this.authService.clearIdentity();
this.router.navigate("/", { replace: true, trigger: false });
this.router.reset();
this.aurelia.setRoot("public/public/public");
}
I have the function forceReturnToPublic() however I want to go and cancel the next() then go directly to the other router... I dont want to redirect..
How do I cancel the next in the promise and reset the router?
Here is my boot.ts which should kick it back to public but I dont know how to jump out of the promise cleanly...
// After starting the aurelia, we can request the AuthService directly
// from the DI container on the aurelia object. We can then set the
// correct root by querying the AuthService's checkJWTStatus() method
// to determine if the JWT exists and is valid.
aurelia.start().then(() => {
var auth = aurelia.container.get(AuthService);
let root: string = auth.checkJWTStatus() ? PLATFORM.moduleName('app/app/app') : PLATFORM.moduleName('public/public/public');
aurelia.setRoot(root, document.body)
});
If I place the forceReturnToPublic() in place of the return next.cancel(new Redirect('login') it goes into and endless loop with errors.
EDIT
I found THIS question which indicates I should add "this.pipeLineProvider.reset()" so I did - like this...
forceReturnToPublic() {
this.pipelineProvider.reset();
this.authService.clearIdentity();
this.router.navigate("/", { replace: true, trigger: false });
this.router.reset();
this.aurelia.setRoot("public/public/public");
}
Whilst it goes straight back to the public route I get an error in the console.
aurelia-logging-console.js:47 ERROR [app-router] Error: There was no router-view found in the view for ../components/clients/clientList/clientList.
at _loop (aurelia-router.js:281)
at NavigationInstruction._commitChanges (aurelia-router.js:307)
at CommitChangesStep.run (aurelia-router.js:143)
at next (aurelia-router.js:112)
at iterate (aurelia-router.js:1272)
at processActivatable (aurelia-router.js:1275)
at ActivateNextStep.run (aurelia-router.js:1161)
at next (aurelia-router.js:112)
at iterate (aurelia-router.js:1191)
at processDeactivatable (aurelia-router.js:1194)
I was clicking on the clientList nav link which does have a router-view..
How/where do I place the pipelineProvider.reset()? (if this is the problem)
But what I really want is...
How do I stop this router and cleanly move to the other router?
As you do I keep trying things.. This worked for me as I believe the pipelineProvider needed to finish before moving to the next step.. So I used a promise like so:
forceReturnToPublic(): Promise<any> {
return Promise.resolve()
.then(() => this.pipelineProvider.reset())
.then(() => this.authService.clearIdentity())
.then(() => this.router.navigate("/", { replace: true, trigger: false }))
.then(() => this.router.reset())
.then(() => this.aurelia.setRoot(PLATFORM.moduleName('public/public/public')));
}
...and no errors.