Vuex, best practice with a global errors and notifications handling - vue.js

here is what i do, and i'am not realy sure its correct :
//store
async addUser({commit}) {
try {
const {data} = await apiService.addUser()
commit('SET_USER', data)
commit('SET_NOTIFICATION', {type:'success', message: 'user successfuly created'})
} catch (error) {
commit('SET_NOTIFICATION', {type:'error', message:error})
}
}
SET_USER(state, user) {
state.users.push(user)
}
//my component:
async addUser() {
this.isLoading = true
await this.$store.dispatch('updatePatient', this.form)
this.isLoading = false
}
is it legit ?
sometimes i think i would need more logic inside my component depending on the succes or rejected api request. Should i put all the logic in my actions ? like i do at the moment ?
Maybe should I add a status state for each actions, for example :
state {
users: []
postUserSuccess: null
postUserError: false
updateUserSuccess: null
updateUserError: false
// ...
}
and do what i want in the component with a computed property mapped to the store ?
What do you think ?

I don't know if it's a best practice but I let the components the exception handling. That method has its pros (you don't have to pollute the state with error management) and cons (you have to repeat the error management code for every action call).
All service calls will be made in actions
The state will only be set in mutations.
All service calls will return a promise with a resolve(data to load in the state) and a reject(message errors to present).
There will be an interceptor to reject the response in case there's a custom error (here you can put if the response has an error prop reject the response and send as an error the error prop, now you don't have to deconstruct the response in the action).
I'm going to give you a simplified example (I use axios, you can learn how to do it with the library that you use).
Actions in Vuex are asynchronous. So you don't need to try/catch them.
ApiService - Add User
const addUser = () => {
return new Promise((resolve, reject) => {
axios
.post(url, user)
.then(response => resolve(response.data))
.catch(error => reject(error));
});
};
store
async addUser({commit}) {
const data = await apiService.addUser();
commit('SET_USER', data);
return data;
}
if the promise in apiService.addUser is resolved the commit is going to be made if is rejected axios will return the promise and you can catch the error in the component that calls the action.
Component
async addUser() {
this.isLoading = true;
try {
await this.$store.dispatch('updatePatient', this.form);
} catch (error) {
// here goes the code to display the error or do x if there is an error,
// sometimes I store an errors array in the data of the component other times I do x logic
}
this.isLoading = false;
}
State
Your state will be cleaner now that you don't need to store those errors there.
state {
users: []
}

Related

What are the best practices for handling vuex errors?

I'm new to vue. I use interceptors for handling action responses, all easy with successful responses. But I would like to know what are the best practice to handle error responses.
I want to show a toastr with error message from response by default if there's no catch block in the action, but if there is a catch, do only catch function with no toastr shown.
Also, is it ok to handle unauthorized response making a redirect to login page directly in interceptor and what advices can be given about it?
My current interceptor looks like this:
axios.interceptors.response.use(
(response) => {
return response.data.data;
},
(error: AxiosError) => {
const data = error.response?.data;
const code = data?.code;
if (code === ErrorCodes.NEED_EMAIL_CONFIRMATION) {
router.push("email-verification").then();
} else if (code === ErrorCodes.UNAUTHORIZED) {
router.push("sign-in").then();
} else {
if (undefined !== data.error) {
toaster.error(data.error);
} else {
toaster.error(i18n.t("unexpected"));
}
}
return error;
}
);
but I don't like too many responsibilities here and I don't know how to avoid toastr show when the action has a catch function
You can control error toast notification from where you send the request, by sending an extra config.
Using axios:
axios.post('/api-name', data, {
config: {
showToast: true,
},
})
and then on axios intercept:
axios.interceptors.response.use(
response => {...},
error => {
const showTost= error.config.errorToast
if(showToast){
// show toast you can pass custom message too...<3
}
}

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.

Debounce Vuex Action Call to Database Not Working

I have a few components that can be separate or on the same page. Each of these components uses the same Vuex state. Since they can each be used on other pages and still work, each of them dispatches a call to the same Vuex action which in turns calls a service that uses axios to get the JSON data.
All of this works great!
However, when I do have 2 (or more) of these components on a single page, that axios call gets called 1 time for each of the components. Initially, I went down the path of trying to see if data existed and get created a "last got data at" timestamp so I could just bypass the 2nd call. However, these are happening both on the components created event and are being essentially called at the same time.
So, enter debounce. Seems like the exact reason for this. However, when I implement it, it fails and is passing on to the next line of code and not awaiting. What am I doing wrong?
Agenda Component (one that uses the same state)
async created() {
await this.gatherCalendarData();
},
methods: {
async gatherCalendarData() {
await this.$store.dispatch('time/dateSelected', this.$store.state.time.selectedDate);
},
},
Month Component (another, notice they are the same)
async created() {
await this.gatherCalendarData();
},
methods: {
async gatherCalendarData() {
await this.$store.dispatch('time/dateSelected', this.$store.state.time.selectedDate);
},
},
The Action getting called
async dateSelected(context, data) {
let result = await getCalendarData(isBetween.date, context.rootState.userId);
await context.commit('SET_MONTHLY_DATA', { result: result.Result, basedOn: isBetween.date });
},
This getCalendarData method is in a service file I created to make api calls (below.)
This is the error that I receive (once for each component) that calls this action.
[Vue warn]: Error in created hook (Promise/async): "TypeError: Cannot read property 'Result' of undefined"
Which is referring to the 3rd line above: result: result.Result
API Service
const getCalendarData = debounce(async (givenDate, userId) => {
let response = await getCalendarDataDebounced(givenDate, userId);
return response;
}, 100);
const getCalendarDataDebounced = async (givenDate, userId) => {
let result = await axiosGet('/api/v2/ProjectTime/BuildAndFillCalendarSQL', {
givenDate: givenDate,
userID: userId,
});
return result;
};
Axios Wrapper
const axiosGet = async (fullUrl, params) => {
let result = null;
try {
let response = await axios.get(fullUrl, params ? { params: params } : null);
result = await response.data;
} catch(error) {
console.error('error:', error);
}
return result;
};
If I put console.log messages before, after and inside the getCalendarData call as well as in the getCaledarDataDebounced methods: (assuming just 2 components on the page) the 2 before logs show up and then the 2 after logs appear. Next the error mentioned above for each of the 2 components, then a single 'inside the getCalendarData' is logged and finally the log from within the debounced version where it actually gets the data.
So it seems like the debouncing is working in that it is only run a single time. But it appears that await call let result = await getCalendarData(isBetween.date, context.rootState.userId); is not truly Waiting.
Am I missing something here?
EDITS after Answer
Based on #JakeHamTexas' answer, my action of dateSelected is now (actual full code, nothing removed like above as to not confuse anything):
async dateSelected(context, data) {
console.log('dateSelected action');
let isBetween = isDateWithinCurrentMonth(data, context.state);
if (!isBetween.result) {
// The date selected is in a different month, so grab that months data
return new Promise(resolve => {
getCalendarData(isBetween.date, context.rootState.userId)
.then(result => {
console.log('inside promise');
context.commit('SET_MONTHLY_DATA', { result: result.Result, basedOn: isBetween.date });
context.commit('SET_SELECTED_DATE', isBetween.date);
context.commit('statistics/TIME_ENTRIES_ALTERED', true, { root: true });
resolve();
});
});
} else {
// The date selected is within the given month, so simply select it
context.commit('SET_SELECTED_DATE', data);
}
context.commit('CLEAR_SELECTED_TIME_ENTRY_ID');
},
And my API call of getCalendarData is now:
const getCalendarData = async (givenDate, userId) => {
console.log('getting calendar data');
let result = await axiosGet('/api/v2/ProjectTime/BuildAndFillCalendarSQL', {
givenDate: givenDate,
userID: userId,
});
return result;
};
The error is gone! However, it does not seem to be debouncing - meaning everything gets called 3 times. I would expect the dateSelected action to be called 3 times. But I would like to avoid the getting calendar data being called 3 times. If it helps, this is what the console looks like:
dateSelected action
getting calendar data
dateSelected action
getting calendar data
dateSelected action
getting calendar data
inside promise
inside promise
inside promise
You need to return a promise from your action. Returning a promise of undefined (which is what is currently happening) resolves immediately.
dateSelected(context, data) {
return new Promise(resolve => {
getCalendarData(isBetween.date, context.rootState.userId)
.then(result => {
context.commit('SET_MONTHLY_DATA', { result: result.Result, basedOn: isBetween.date });
resolve();
}
}
},
Additionally, a vuex commit does not return a promise, so it doesn't make sense to await it.

Check if the dispatch vuex is done

Hi I have this create and update zone function. After the API call success. I will callback again the dispatch on vuex store. Then Go back to main zone page.
The problem is it will took around 5 secs to get the list the results of dispatch. Making my list not updated.
How to know if the dispatch is done before going back to the page?
loadZones(){
this.$store.dispatch('getZones');
},
createOrUpdateZone(zone, region_checkbox, callback){
this.$http.post(process.env.API_URL +'/api/.....)
.then(res=> {
if(res.data.success == true){
this.loadZones();
this.$router.push('/zone');
} else{
this.has_error = true;
})
}
Vuex actions always return Promise, just add return when you create request in your getZones action to chain your ajax request promise with returned by action, then you can do something like this:
//... in actions, example
getZones(context) {
return some_http_request()
}
//...
loadZones(){
return this.$store.dispatch('getZones');
},
createOrUpdateZone(zone, region_checkbox, callback){
this.$http.post(process.env.API_URL +'/api/.....)
.then(res=> {
if(res.data.success == true){
// next "then" will be invoked when this request will be done
return this.loadZones();
}
else throw new Error();
})
.then(() => {
this.$router.push('/zone');
})
.catch(() => this.has_error = true);
}
You can use async await.
When you make loadZones async function, in it you can use await on the dispatch getZones. But remember that the getZones action should return a promise. I believe that it already returning a promise, so you just have to add async await.
async loadZones(){
await this.$store.dispatch('getZones');
},
createOrUpdateZone(zone, region_checkbox, callback){
this.$http.post(process.env.API_URL +'/api/.....)
.then(res=> {
if(res.data.success == true){
this.loadZones();
this.$router.push('/zone');
} else{
this.has_error = true;
})
}

how to pass a reference to a component when calling a vuex action

I'm fairly new to vue (and very new to vuex). I would like to move some axios api calls to be actions in my Vuex store. I know have for example:
actions:{
LOAD_USER: function ({ commit }) {
axios.get('/arc/api/v1/me', {dataType: 'json'})
.then((response )=> {
commit('SET_USER', { user: response.data.user })
})
.catch(function (error) {
console.log(error.message);
});
and call this in my calling component via:
this.$store.dispatch('LOAD_USER')
and this is working. My problem is that I need to set some variables in the calling component to false or kill a progress bar. Here's what I was previously using in my calling component:
this.loading = true
this.$Progress.start()
axios.get('/arc/api/v1/me', {dataType: 'json'})
.then((response )=> {
this.$Progress.finish()
this.loading = false
this.$store.state.user = response.data.user;
this.user = this.$store.state.user
})
.catch(function (error) {
this.$Progress.fail()
console.log(error.message);
});
How would I integrate these loading behaviors into my vuex action? How would I pass a reference to my component via this call:
this.$store.dispatch('LOAD_USER')
or is there a better solution?
Well, you can always use the second parameter of Store.dispatch() to pass any payload into the corresponding action:
this.$store.dispatch('LOAD_USER', this); // passing reference as payload
... but I strongly recommend against doing this. Instead, I'd rather have the whole state (including 'loading' flag, etc.) processed by VueX.
In this case, a single action - LOAD_USER, based on asynchronous API request - would commit two mutations to Store: the first one sets loading flag when the request has been started, the second one resets it back to false - and loads the user data. For example:
LOAD_USER: function ({ commit }) {
commit('LOADING_STARTED'); // sets loading to true
axios.get('/arc/api/v1/me', {dataType: 'json'})
.then(response => {
commit('LOADING_COMPLETE'); // resets loading flag
commit('SET_USER', { user: response.data.user });
})
.catch(error => {
commit('LOADING_ERROR', { error }); // resets loading
console.log(error.message);
});
This approach, among the other advantages, simplifies things a lot when your requests' logic gets more complicated - with error handling, retries etc.
Actions can return a promise https://vuex.vuejs.org/en/actions.html
I think what you want to do is activate the loading when you call your action and stop the loading when the promise is resolved or rejected.
// Action which returns a promise.
actions: {
LOAD_USER ({ commit }) {
return new Promise((resolve, reject) => {
axios.get('/arc/api/v1/me', {dataType: 'json'})
.then((response )=> {
commit('SET_USER', { user: response.data.user })
resolve()
})
.catch(function (error) {
console.log(error.message);
reject(error);
});
})
}
}
// Update loading when the action is resolved.
this.loading = true;
store.dispatch('LOAD_USER').then(() => {
this.loading = false;
})
.catch(function(error) {
// When the promise is rejected
console.log(error);
this.loading = false;
});
If you can't achieve your goal using the above you can add the loading boolean to your vuex store and import it in your component. Than modify the loading boolean inside your action (using mutations) to let the view update.
Note: I would not pass a reference to your actions. While this is possible there are likely better solutions to solve your problem. try to keep the view logic in your components whenever possible.