In this article we're using Vue 2. I can’t guarantee that what's referred here is considered to be a good practice anymore, as I’m mostly involved with React these days.
Demo
Preface
Async - await is a killer feature introduced in ES7.
What I really don’t like, is having to wrap everything in a try-catch block. The code seams really busy, and repeating it everytime for every async action is a no go.
Solution
Let’s start by using a call that may or may not, resolve successfully.
const requestUsers = () => fetch('https://jsonplaceholder.typicode.com/users');
Normally we would write something like this
const doTheCall = async () => {
try {
const users = await requestUsers();
} catch (e) {
// something with the error
}
};
Instead of that, we will wrap the request with another function which will handle any error.
export const wrapRequest =
(fn) =>
(...params) =>
fn(...params)
.then((response) => {
if (!response.ok) {
throw response;
}
return response.json();
})
.catch((error) => handleError(error));
What 'HandleError' can do practically depends totally on your needs.
For now, let’s assume that we catch the error and generate the appropriate message for each case. We will dispatch an action that will populate some error messages and call it a day.
import store from './store';
const handleError = (error) => {
const errorStatus = error ? error.status : error;
const errorMessage = prepareErrorMessage(errorStatus);
store.dispatch('populateErrors', errorMessage);
};
Vuex Setup
Our vuex setup will be the following:
import Vue from 'vue';
import Vuex from 'vuex';
import errors from './_errors.js';
import users from './_users.js';
import loader from './_loader.js';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
errors,
users,
loader,
},
});
Where the users module is as simple as that
import {wrappedRequestUsers} from '../requests';
const state = {
usersList: [],
};
const getters = {
usersList: (state) => state.usersList.length,
};
const mutations = {
usersListSet: (state, list) => (state.usersList = list),
updateLoader: (state, status) => (state.loading = status),
};
const actions = {
requestUsers: async ({commit}) => {
const data = await wrappedRequestUsers();
if (data) commit('usersListSet', data);
},
clearUsersList: ({commit}) => {
commit('usersListSet', []);
},
};
export default {
state,
getters,
mutations,
actions,
};
As for the error handling actions, we will push the new error message in the state
const state = {
errors: [],
};
const getters = {
errors: (state) => state.errors,
};
const mutations = {
addError: (state, error) => state.errors.unshift(error),
popError: (state) => state.errors.pop(),
};
const actions = {
populateErrors: ({commit}, error) => {
commit('addError', error);
setTimeout(() => commit('popError'), 3000);
},
};
export default {
state,
getters,
mutations,
actions,
};
And the custom toastr component will simply loop through every error message
<template>
<div class="error-wrapper">
<transition-group name="fade" tag="div">
<div class="error" v-for="(error, index) in errors" :key="index">
{{ error }}
</div>
</transition-group>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: 'errorToastr',
computed: {
...mapGetters(['errors']),
},
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.error-wrapper {
position: absolute;
top: 0;
right: 0;
.error {
background: #cc0000;
border-radius: 8px;
color: #fff;
margin-top: 1em;
padding: 0.5em 2em;
}
}
</style>
Use a spinner
Sometimes though, we have to display a spinner. For this reason, I’ve made a separate module for the loading instance.
When a loader is needed, we won’t call the action directly, but instead we’ll dispatch executeWithLoader
with the action name as a param.
const state = {
loading: 0,
};
const getters = {
loading: (state) => state.loading > 0,
loadingStatus: (state) => (state.loading > 0 ? 'Fetching stuff' : 'Ready'),
};
const mutations = {
updateLoader: (state, loading) =>
(state.loading = loading ? state.loading++ : state.loading--),
};
const actions = {
executeWithLoader: async ({commit, dispatch}, fn) => {
commit('updateLoader', true);
await dispatch(fn, {root: true});
commit('updateLoader', false);
},
};
export default {
state,
getters,
mutations,
actions,
};
<button
@click='executeWithLoader("requestUsers")'
:disabled="loading"
class="button button--success"
>
Fetch users
</button>