How to handle ajax errors reusably with Vuex? - vue.js

I am building an SPA and I have a couple of different forms that submit data to an API. I am using axios for the ajax calls and have built a wrapper class around it for my use-case, called api. Inside that class I handle errors thrown by that instance.
The problem is I was storing an instance of the api class in each form's state. I later realized that functions shouldn't live in the state due to serialization.
The reasoning behind having the api class in the state was so that all of the children components of a form could access the errors and display their respective error along with removing the error on update.
One solution could be using an axios interceptor and commit all errors to a global errors module. But then I wouldn't know which errors belong to which form, in case two forms (or other requests) were submitted at the same time. I could of course save the errors in regard to the request URI, but then I would also have to take the request method into consideration.
reportError(state, { uri, method, errors }) {
state.errors[uri + '#' + method] = errors;
}
Then I could have a getter like:
getErrorsByRequest: state => ({ uri, method }) => {
return ...
}
But this feels unnecessarily awkward.
Since what I am trying to achieve is most likely very common, I am wondering, how do I sanely handle ajax errors reusably with Vuex?

I was checking for my old projects, and i did something similar:
This is my axios instance interceptor:
axiosInstance.interceptors.response.use(response => response, error => {
const { status } = error.response
...
...
// data invalid (Unprocessable Entity)
if (status === 422) {
// errors list from response
const dataErrors = error.response.data.errors
let objErrors = {}
// joining just the first error from array errors as value for each prop
for (let key in dataErrors) {
objErrors[key] = dataErrors[key].join()
}
// commiting to errors module
store.commit('errors/SET_ERRORS', objErrors)
}
...
...
})
Here my store module errors:
export const state = {
form_errors: {}
}
export const mutations = {
SET_ERRORS: (state, errors) => { state.form_errors = errors },
CLEAN_ERRORS: (state) => { state.form_errors = {} }
}
Using on components:
computed: {
...mapState('errors', ['form_errors'])
}
Using on form template:
<q-input
v-model="form.description"
:error-message="form_errors.description"
:error="!!form_errors.description"
/>

Related

Vue2 Composition Api - How do I fetch data from api?

I am using Vue2.6 with composition api.
I need to reroute to different pages depends on an api response.
Can someone please guide me, please?
I tried using onBeforeMount but it renders the UI elements then rerouted to the corresponding page to the api response..so I can see a flash of the wrong UI..
setup() {
const myData = 'myData';
onBeforeMount(async () => {
try {
const results = await fetchData();
// do reroute depends on results response
}
} catch (err) {
console.log(err);
}
});
return {
myData,
};
I also tried adding async in the setup method but it errored saying my ref variables "Property or method "myData" is not defined on the instance but referenced during render."
async setup() {
const myData = 'myData';
onMounted(async () => {
try {
const results = await fetchData();
// do reroute depends on results response
}
} catch (err) {
console.log(err);
}
});
return {
myData,
};
It looks like you're trying to handle routing (re-routing) dynamically from inside a component. I can't see the rest of the apps, so can't speak to the validity of such a solution, but would like you dissuade you from doing that. routing logic should, IMO, not be handled in a component. The components should mostly just handle the template and user interaction. By the time you're rendering a component, that API should have been resolved already.
I would recommend to resolve the API response before the route is
even completed. You or use a navigationGuard to resolve the API during the route execution. This functionality is asynchronous, so you can await the response before proceeding.
Alternatively, if you really want to handle it in the component, you will have that delay while the API is resolving, but you can implement some loader animation to improve the experience.

Computed params with skip/enable

I was getting annoyed that some of my apollo requests were working and some were not. The ones that don't seem to work are requests with computed params.
Here is an example of one that does work:
import { computed } from "#vue/composition-api";
import * as getCategoryBySlug from "#graphql/api/query.category.gql";
import { useGraphQuery } from "./graph-query";
export function useGetCategory(context) {
const params = computed(() => {
const slug = context.root.$route.params.categorySlug;
if (!slug) return;
return { slug };
});
const { response, error, loading } = useGraphQuery(
params,
getCategoryBySlug,
(data) => data.categoryBySlug
);
return { category: response, categoryError: error, categoryLoading: loading };
}
As I am computing my params on the categorySlug, it is available on the route, so it should never be null/undefined.
My useGraphQuery method looks like this:
import { useQuery, useResult } from "#vue/apollo-composable";
export function useGraphQuery(params, gql, pathFn, clientId = "apiClient") {
// if (!params?.value)
// return {
// response: ref(undefined),
// loading: ref(false),
// error: ref(undefined),
// query: ref(undefined),
// };
// TODO: figure our a way to skip the call if the parameters are null
const { result, loading, error, query, fetchMore } = useQuery(gql, params, {
clientId,
//enabled: !!params?.value,
});
const response = useResult(result, null, pathFn);
return { response, loading, error, query, fetchMore };
}
As you can see, I am having an issue because I can't skip and enabled doesn't seem to work as a suitable workaround (for skip).
I tried to return a reference if the parameters are null/undefined, but this never tried to execute the query if the computed params became available.
So my question is how can I skip the request or wait until the params are available?
You should consider changing the flow that calls this method, so it would be called only when the params are defined, instead of trying to skip it when not ready and try to retrigger it again from within the method. Most of the time this will make the code clearer, and also make it more resource-efficient as it won't make unneeded method calls.
If you depend on user input try to validate the input is ready before calling this method.
You can also add a watcher on the params that will trigger the flow when they change, and check in the watcher that all the relevant values are defined before calling the method.
Of course, if you can use computed variables it is better than using watchers in most cases, but in some cases, it can help (mostly when the variable is calculated by an async function, for example, the use of apollo request).

How to get access to store from action in VueJS?

Im using VueJS and Vuex. I have the userid into the store, this way:
vuex screenshot
And i try pass the userid to a fetch, but vuejs return error
([Vue warn]: Error in created hook: "TypeError: this.$store is
undefined")
import { LOAD_APPOINTMENTS } from './types'
export default {
loadProducts ({ commit }) {
var user = this.$store.state.user.userid
fetch('api/appointments/' + user)
.then((result) => {
return result.json()
})
.then((appointments) => {
commit(LOAD_APPOINTMENTS, appointments)
})
.catch(er => {
console.log(er)
})
}
}
First, when referencing the store within vuex files:
context.state instead of this.$store.state.
context for all of the this.$store. So, context.commit and context.dispatch.
Second, the loadProducts needs to be rewritten as an action per docs.
Third, loadProducts needs to incorporate the context as a parameter:
actions: {
loadProducts (context) {
...
context.commit(...)
...
}
}
As #phil has mentioned in this thread, it is important to view the documentation entirely, as this single answer will get you on the way to debugging the problem, but there might be multiple more problems showing up (e.g. fetch errors, file structure errors, component/App level errors).

My saga function is not being called at all when calling my action?

I joined a big/medium project, I am having a hard time creating my first redux-saga-action things, it is going to be a lot of code since they are creating a lot of files to make things readable.
So I call my action in my componentDidMount, the action is being called because I have the alert :
export const fetchDataRequest = () => {
alert("actions data");
return ({
type: FETCH_DATA_REQUEST
})
};
export const fetchDataSuccess = data => ({
type: FETCH_DATA_SUCCESS,
payload: {
data,
},
});
This is my history saga : ( when I call the action with this type, The function get executed )
export default function* dataSaga() {
// their takeEverymethods
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
}
This is what has to be called : ( I am trying to fill my state with data in a json file : mock )
export default function* fetchTronconsOfCircuit() {
try {
// Cal to api
const client = yield call(RedClient);
const data = yield call(client.fetchSomething);
// mock
const history = data === "" ? "" : fakeDataFromMock;
console.log("history : ");
console.log(history);
if (isNilOrEmpty(history)) return null;
yield put(fetchDataSuccess({ data: history }));
} catch (e) {
yield put(addErr(e));
}
}
And this is my root root saga :
export default function* sagas() {
// many other spawn(somethingSaga);
yield spawn(historySaga);
}
and here is the reducer :
const fetchDataSuccess = curry(({ data }, state) => ({
...state,
myData: data,
}));
const HistoryReducer = createSwitchReducer(initialState, [
[FETCH_DATA_SUCCESS, fetchDataSuccess],
]);
The method createSwitchReducer is a method created by the team to create easily a reducer instead of creating a switch and passing the action.type in params etc, their method is working fine, and I did exactly what they do for others.
Am I missing something ?
I feel like I did everything right but the saga is not called, which means it is trivial problem, the connection between action and saga is a common problem I just could not figure where is my problem.
I do not see the console.log message in the console, I added an alert before the try-catch but got nothing too, but alert inside action is being called.
Any help would be really really appreciated.
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
should be
yield takeEvery(FETCH_DATA_REQUEST, fetchTronconsOfCircuit);

Vuex, best practice with a global errors and notifications handling

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: []
}