vue.js, axios, interceptors and toast-notifications
I usually use axios when it comes to Asynchronous JavaScript and XML (ajax) or more modern Asynchronous JavaScript and JSON (ajaj). I like that axios runs in the browser and in node.js, and with a promise polyfill in IE11 as well - and projects I usually do, do support IE11.
In my recent projects, I wanted to add toast-notifications, these little jump-in-notifications, telling my visitor that the app is doing something or something went wrong.
Besides notifications like "you're offline / you're online / your connection speed changed" I wanted to inform my visitor of api calls with toasts. Not every api call, not instantly, but sometimes. And this is where axios interceptors really come in handy.
A full working example of this blog post is available here: vue-axios-interceptor.
project setup
Starting point is a vue.js project setup with @vue/cli and the vue-axios extension, and vue-toastification (the toasting comes later in that post):
// /src/main.js
import Vue from 'vue';
import VueAxios from 'vue-axios';
import axios from 'axios'; // this is all about to change
…
import App from './App.vue';
…
Vue.use(VueAxios, axios);
…
new Vue({
…
}).$mount('#app');
Vue-axios takes an axios instance and makes it available everywhere in your app, so you can call this.axios.get/post/delete/update
or this.$http…
at any time in your app.
custom axios interceptor configuration
Axios interceptors can setup some request and response handling globally in your app. In my example I use interceptors to measure the time an api call takes, and to show a toast "fetching data" automatically, so I do not need to add this function on every API call. In a component or view you type this.axios.get(...)
and the interceptor will do their work.
First create a src/plugins/axios.js
file and configure your interceptors and more:
// /src/plugins/axios.js
import axios from 'axios';
// doing something with the request
axios.interceptors.request.use(
(request) => {
// do something with request meta data, configuration, etc
…
// dont forget to return request object,
// otherwise your app will get no answer
return request;
}
);
// doing something with the response
axios.interceptors.response.use(
(response) => {
// all 2xx/3xx responses will end here
return response;
},
(error) => {
// all 4xx/5xx responses will end here
return Promise.reject(error);
}
);
export default axios;
I hope you see the power axios lays in your hands here.
exchange some data between request and response
For a bit we stay in that clear interceptor context, because you can add data in the request object, which will end up in your response object.
For example a timestamp if you want to measure the speed of your api, or something else you create on request start and want to reuse on response:
// /src/plugins/axios.js
import axios from 'axios';
axios.interceptors.request.use(
(request) => {
// eslint-disable-next-line no-param-reassign
request.config = {
...(request.config ?? {}), // preserve a given request.config object
start: Date.now(),
};
return request;
},
);
axios.interceptors.response.use(
(response) => {
const now = Date.now();
console.info(`Api Call ${response.config.url} took ${now - response.config.config.start}ms`);
return response;
},
(error) => Promise.reject(error),
);
export default axios;
use custom axios instance
Instead of importing "native" axios, we import our plugins/axios instance and use this with vue-axios:
// /src/main.js
import Vue from 'vue';
import VueAxios from 'vue-axios';
…
import axios from './plugins/axios';
…
Vue.use(VueAxios, axios);
…
App Axios Toasts
With that setup, we can now use toast messages to inform our users when an api call is started. I will use vue-toastification for toasting:
// /src/main.js
import Vue from 'vue';
import VueAxios from 'vue-axios';
import Toast from 'vue-toastification';
import axios from './plugins/axios';
import App from './App.vue';
import 'vue-toastification/dist/index.css';
Vue.config.productionTip = false;
Vue.use(VueAxios, axios);
Vue.use(Toast);
new Vue({
render: (h) => h(App),
}).$mount('#app');
tell axios to show or not to show a toast
By starting an axios call in our app, we will provide some config data, so our axios interceptor can decide if a toast is needed or not:
// some component.vue
export default {
…
methods: {
loadData() {
this.axios.get(
`/route/to/my/api/${this.$route.param.id}`,
{
config: {
showToast: true,
requestToast: {
title: 'Loading Data',
},
responseToast: {
title: 'Data loaded!',
},
})
.then(({ data }) => {
this.data = data;
})
.catch((error) => {
console.error('api call failed', error);
});
},
},
…
};
Let's look at our axios interceptor file:
// /src/plugins/axios.js
import Vue from 'vue';
import axios from 'axios';
axios.interceptors.request.use(
(request) => {
// eslint-disable-next-line no-param-reassign
request.config = {
showToast: false, // may be overwritten in next line
...(request.config || {}),
start: Date.now(),
};
if (request.config.showToast) {
// eslint-disable-next-line no-param-reassign
request.config.requestToastId = Vue.$toast(
request.config.requestToast.title,
);
}
return request;
},
);
axios.interceptors.response.use(
(response) => {
const now = Date.now();
const request = response.config;
console.info(`Api Call ${request.url} took ${now - request.config.start}ms`);
if (request.config.requestToastId) {
Vue.$toast.dismiss(request.config.requestToastId);
}
if (request.config.showToast && request.config.responseToast) {
Vue.$toast(request.config.responseToast.title);
}
return response;
},
(error) => Promise.reject(error),
);
export default axios;
So - we keep the call measurement and if a toast is configured, we show that toast, keep the toast id, so we can dismiss that toast on response and show a success toast to our users.
In the last step, I import
ed the Vue
instance to the axios file, so I have access to the $toast
component. Another usecase could be to handle unauthorized api calls, or other errors within an axios call.
Have a look at my example vue-axios-interceptor project with a full working app, consuming the dog api 🐕.
Article Image from Chris Ovalle via unsplash and ghost ♥.