Vue3 - Using beforeRouteEnter to prevent flashing content - vue.js

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('/')
}
})
}

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.

ASync/Await is not working as expected in router.BeforeEach guard in vue?

this is my router guard :
router.beforeEach(async (to,from,next)=>{
await store.dispatch('GetPermission');
if(to.matched.some(record => record.meta.requireAuth)){
let permissions=store.state.permissions; //getting empty
console.log(permissions);
if(permissions.filter(per => (per.name === 'read_list').length!=0)){
next({
path:'/dashboard/create'
})
}
else{
next()
}
}
// else if(to.matched.some(record => record.meta.requireAuth)){
// if(store.token!=null){
// next({
// path:'/dashboard'
// })
// }
// else{
// next()
// }
// }
else{
next()
}
});
problem is here though i m using await in dispatch method , i m not getting state value of permissions which is initially empty
here is vuex store code :
GetPermission(context){
axios.defaults.headers.common['Authorization']='Bearer ' + context.state.token
axios.get('http://127.0.0.1:8000/api/user').then((response)=>{
console.log(response)
context.commit('Permissions',response.data.permission)
})
//mutation:
Permissions(state,payload){
state.permissions=payload
}
//state
state:{
error:'',
token:localStorage.getItem('token') || null,
permissions:'',
success:'',
isLoggedin:'',
LoggedUser:{}
}
help me to solve it please ??
actions in Vuex are asynchronous. The only way to let the calling function (initiator of action) to know that an action is complete - is by returning a Promise and resolving it later.
Here is an example: myAction returns a Promise, makes a http call and resolves or rejects the Promise later - all asynchronously
actions: {
myAction(context, data) {
return new Promise((resolve, reject) => {
// Do something here... lets say, a http call using vue-resource
this.$http("/api/something").then(response => {
// http success, call the mutator and change something in state
resolve(response); // Let the calling function know that http is done. You may send some data back
}, error => {
// http failed, let the calling function know that action did not work out
reject(error);
})
})
}
}
Now, when your Vue component initiates myAction, it will get this Promise object and can know whether it succeeded or not. Here is some sample code for the Vue component:
export default {
mounted: function() {
// This component just got created. Lets fetch some data here using an action
this.$store.dispatch("myAction").then(response => {
console.log("Got some data, now lets show something in this component")
}, error => {
console.error("Got nothing from server. Prompt user to check internet connection and try again")
})
}
}
Also,you are calling same route when no permission match, in that case it always call your same route and make infinite loop.
Redirect to access denied page if permission denied.

Vue Router - Accessing the Vue instance without calling next()

I'm hacking at a way to push my SPA to a route if the for instance the validation fails. I'm using the beforeRouteEnter and beforeRouteUpdate methods to run a function to determine this;
...
beforeRouteEnter(to, from, next) {
loading(next);
},
beforeRouteUpdate(to, from, next) {
loading(next);
},
...
And then this function to validate (this is just temporary until I have this figured out).
let loading = function(callback) {
db.commit('setPageLoading', true);
new requests().concat([
'UserController#userDetails',
'SettingController#settings',
'ClientController#allClients',
'StatsController#statsUsage',
'UserController#paymentDetails'],
() => {
db.commit('setPageLoading', false);
/** everything's all gravy, `next()` should be called now! */
callback();
},
(error) => {
/**
* I dont want to call `next()` to access the vue instance as it will show the requested page
* but I have to to access the router!!!
**/
callback(vm => {
switch(error.response.status) {
case 401:
/** Put login box here, this will do for now **/
vm.$router.push('/login').catch(err => {});
break;
/** Custom error to tell the SPA the user needs to fill in their company details first */
case 498:
for(let i in error.response.data.errors) {
db.commit('addErrorNotification', error.response.data.errors[i][0]);
}
vm.$router.push('/profile').catch(err => {});
break;
}
});
});
};
My problem being I have to call next()to access the vue instance to push to a different route. But my calling the next() callback, it tells Vue Router to carry on loading the current or pending route which is not what I want to happen.
Can anyone tell me a way how I can access the Vue instance without calling the next() callback?
Regards
As described by Delena Malan, thank you!;
let loading = function(callback) {
db.commit('setPageLoading', true);
new requests().concat([
'UserController#userDetails',
'SettingController#settings',
'ClientController#allClients',
'StatsController#statsUsage',
'UserController#paymentDetails'],
() => {
db.commit('setPageLoading', false);
/** everything's all gravy, `next()` should be called now! */
callback();
},
(error) => {
switch(error.response.status) {
case 401:
/** Put login box here, this will do for now **/
callback({ path: '/login' });
break;
case 498:
for(let i in error.response.data.errors) {
db.commit('setNotifications', []);
db.commit('addErrorNotification', error.response.data.errors[i][0]);
}
callback({ path: '/profile' });
break;
}
db.commit('setPageLoading', false);
});
};

vue router next() function don't work in Promise in router.beforeEach

I have the following code:
router.beforeEach((to, from, next) => {
if (to.name !== from.name) {
store
.dispatch("fetchCurrentUser")
.then(() => {
console.log('then');
// do something
next();
})
.catch(() => {
console.log('catch');
router.push("/login");
next();
});
} else {
next();
}
// next();
});
I'm trying to get the current user, and if this succeeds, then do something with this data, and if the request is not successful, then redirect the user to the login page. But next () calls do not work, I get the "then" or "catch" in the console, but the redirect does not occur and an infinite loop begins. But if I take next () from condition (commented row) the redirect works fine.
To redirect you should use next('/') or next({ path: '/' }).
From the documentation:
next: Function: this function must be called to resolve the hook. The
action depends on the arguments provided to next:
next(): move on to the next hook in the pipeline. If no hooks are
left, the navigation is confirmed.
next(false): abort the current navigation. If the browser URL was
changed (either manually by the user or via back button), it will be
reset to that of the from route.
next('/') or next({ path: '/' }): redirect to a different location.
The current navigation will be aborted and a new one will be started.
You can pass any location object to next, which allows you to specify
options like replace: true, name: 'home' and any option used in
router-link's to prop or router.push
The promise resolves after the function ends.
This means that the commented next happens regardless of the result of the promise result. Then the promise resolves and you call another next.
The bottom line is that you don't need the commented next and should just cover the promise resolve.
I was able to implement an async validation inside beforeEach, authentication in my case.
export async function onBeforeEach(to, from, next) {
let { someUserToken } = to.query
if (someUserToken) {
let nextRoute = await authenticate(to)
next(nextRoute)
} else {
const userToken = store.state.application.userToken
if (!to.meta.public && !userToken) {
next({ name: 'Forbidden' })
} else {
next()
}
}
}
async function authenticate(to) {
let { someUserToken, ...rest } = to.query
return store
.dispatch('application/authenticate', {
someUserToken
})
.then(() => {
return {
name: 'Home',
query: {
...rest
}
}
})
.catch(() => {
return { name: 'Forbidden' }
})
}
I hope this helps.

VueRouter - Fetching Before Navigation - Multiple AJAX

I would like to use the beforeRouteEnter guard so I can be sure my data is loaded before going to a page. I read the example you can find here in the vue-router documentation.
Current situation
I'm more or executing two AJAX calls to get some data in the created lifecycle event.
export default {
created() {
const _this = this;
axios.get('/getCompanyDetails').then((response) => {
_this.private.company_details = response.data
});
axios.get('/getusers').then((response) => {
if(response.data){
_this.private.company_users = response.data;
}
});
}
}
What I try
beforeRouteEnter (to, from, next) {
function getCompanyDetails() {
return axios.get('/getCompanyDetails')
}
function getUsers() {
return axios.get('/getusers');
}
axios.all([getCompanyDetails(), getUsers()])
.then(axios.spread(function (company_details, company_users) {
next(vm => vm.setData(err, company_details, company_users))
}));
},
Am I on the right track ? The only thing I see here is I fell I'm required to call only one function setData in the next with all the parameters received from the different AJAX calls.
Is there a way to call several functions like setUsers(), setDetails() in the next ?
Is there a better way than what I'm doing ?
As #thanksd stated :
next((vm) => { vm.setUser(err, company_users); vm.setDetails(err, company_details); })
The final answer is then
beforeRouteEnter (to, from, next) {
function getCompanyDetails() {
return axios.get('/getCompanyDetails')
}
function getUsers() {
return axios.get('/getusers');
}
axios.all([getCompanyDetails(), getUsers()])
.then(axios.spread(function (company_details, company_users) {
next((vm) => { vm.setUser(err, company_users); vm.setDetails(err, company_details); })
}));
},