I use HTTP requests to get data for my Vue.js application. I have one file called Api.js with the base axios instance:
export default () => {
return axios.create({
baseURL: apiURL,
headers: {
Authorization: `JWT ${store.state.token}`
}
})
}
than I have a file called service.js, which contains the functions for the different endpoints:
export default {
status() {
return Api().get('status/')
}
}
In the .vue file I call the method like that.
created() {
Service.status()
.then(response => {
// do something with the data
})
.catch(e => {
// show exception
})
}
Some exceptions should be handled in Api.js (for example: 401), some other exceptions should be handled in service.js and others in the .vue file. How can I do that?
Disclaimer: I have created two small axios plugins to achieve this specific pattern easily.
axios-middleware
Simple axios HTTP middleware service to simplify hooking to HTTP requests made through Axios.
It uses axios interceptors as mentioned by acdcjunior but it abstracts the use of axios with a commonly known middleware pattern so your app doesn't need to know and deal with the interceptor syntax.
// import your API's axios instance
import http from './api';
import { Service } from 'axios-middleware';
// Create a new service instance
const service = new Service(http);
// We're good to go!
export default service;
You can then use this middleware service to register different middlewares anywhere in your app. A middleware can be as simple as an object or a reusable, easily testable class.
import i18n from './services/i18n';
import toast from './services/toast';
import service from './services/middleware';
import { ApiErrorMiddleware, OtherMiddleware } from './middlewares';
// Then register your middleware instances.
service.register([
// Middleware class instance
new ApiErrorMiddleware(i18n, toast),
new OtherMiddleware(),
// or a simple object
{
onRequest() {
// handle the request
},
onResponseError(error) {
// handle the response error
}
}
]);
Where the ApiErrorMiddleware would be a simple class with the sole responsibility of showing toast messages on error.
export default class ApiErrorMiddleware {
/**
* #param {VueI18n} i18n instance
* #param {Object} toast message service
*/
constructor(i18n, toast) {
this.toast = toast;
this.i18n = i18n;
}
/**
* #param {Object} error
*/
onResponseError(error = {}) {
const { response } = error;
let key = 'errors.default';
if (response && this.i18n.te(`errors.${response.status}`)) {
key = `errors.${response.status}`;
} else if (error.message === 'Network Error') {
key = 'errors.network-error';
} else {
// TODO log unhandled errors
}
this.toast.error(this.i18n.t(key));
}
}
axios-resource
Simple axios resource class to easily interact with a REST endpoint.
Define a resource class. Here, I added onError and onFetchError as examples for your use-case.
import Resource from 'axios-resource';
export default class UserResource extends Resource {
static URL = 'user/{id}';
// This calls `sync` in the background
fetch() {
return super.fetch.apply(this, arguments)
.catch(err => this.onFetchError(err));
}
onFetchError(err) {
// An error occurred while fetching this resource.
}
onError(err) {
// An error occurred with this resource
}
// called for every actions (fetch, create, patch, delete)
sync() {
return super.sync.apply(this, arguments)
.catch((err) => this.onError(err))
}
}
Then, in api.js, create an instance.
import UserResource from './user';
const user = new UserResource();
// GET https://example.com/api/user/me
user.fetch('me')
.then(({ data }) => {
console.log('User data:', data);
});
The error can be dealt with at every step.
in the onFetchError of this specific resource
in the onError of this resource
in a middleware for the app.
You should add axios interceptors:
Axios Interceptors
You can intercept requests or responses before they are handled by
then or catch.
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
Those can (should) be in your Api.js.
Related
I can't understand why I'm getting this error. I am trying to use the Vuex store in a composition function but it keeps throwing me this error about inject (I'm not even using inject). My app makes an await api call to the backend and if there is an error calls my composition function.
[Vue warn]: inject() can only be used inside setup() or functional components.
inject # runtime-dom.esm-bundler-9db29fbd.js:6611
useStore # vuex.esm-bundler.js:13
useErrorHandling # useErrorHandling.js:5
checkUserExists # auth.js:53
Here is my composition function
import { useStore } from 'vuex'
function useErrorHandling()
{
const store = useStore() // <-- this line
function showError(errorMessage) {
console.log(errorMessage)
}
return { showError }
}
export default useErrorHandling
If I remove this line then it doesn't throw that error
// const store = useStore() // <-- this line
UPDATE: this is how the function is called.
/**
* Check if a user exists in database
*/
static async checkUserExists(data)
{
const { env } = useEnv()
const { jsonHeaders } = useHTTP()
const { showError } = useErrorHandling()
try {
let response = await fetch(`${env('VITE_SERVER_URL')}/auth/check-user-exists`, {
method: 'POST',
body: JSON.stringify(data),
headers: jsonHeaders,
})
if (!response.ok) {
let errorMessage = {
statusText: response.statusText,
statusCode: response.status,
body: '',
url: response.url,
clientAPI: 'api/auth.js # checkUserExists',
}
const text = await response.text()
errorMessage.body = text
showError(errorMessage) // <-- here
return
}
response = await response.json()
return response.user_exists
} catch (error) {
alert('Error occured!')
console.log(error)
}
}
The error is telling you that useStore is only for use inside of components, since the module isn't a component. From the docs:
To access the store within the setup hook, you can call the useStore function. This is the equivalent of retrieving this.$store within a component using the Option API.
To use the store in a module, you can import { store } from the module where it was created:
store.js
export const store = createStore({
...
})
Other module
import { store } from './store'
Background of the Problem:
I am building a React/Redux app that uses redux-thunk and wretch (a fetch wrapper) to handle asynchronous requests.
I have a few search actions that can vary significantly in their load times, causing undesirable behavior.
I have looked into using AbortController(), but it's either cancelling all my requests outright, or failing to cancel the previous request.
example problem:
Request a search for "JOHN", then request a search for "JOHNSON".
Results for "JOHNSON" return first, and then results for "JOHN" return later and overwrite the "JOHNSON" results.
Goal:
Initiating a request should abort previous pending requests.
example desired behavior:
Request a search for "JOHN", then request a search for "JOHNSON".
Upon initiating the request for "JOHNSON", the pending request for "JOHN" is aborted.
Code:
actions.js
The fetchData action gets called via an onClick or by other functions.
import api from '../../utils/request';
export function fetchData(params) {
return dispatch => {
dispatch(requestData());
return api
.query(params)
.url('api/data')
.get()
.fetchError(err => {
console.log(err);
dispatch(apiFail(err.toString()));
})
.json(response => dispatch(receiveData(response.items, response.totalItems)))
}
}
export function requestData() {
return {
type: REQUEST_DATA,
waiting: true,
}
}
export function receiveData(items, totalItems) {
return {
type: RECEIVE_DATA,
result: items,
totalItems: totalItems,
waiting: false,
}
}
export function apiFail(err) {
return {
type: API_FAIL,
error: err,
waiting: false,
}
}
utils/request.js
This is wretch import. Wretch is a fetch wrapper so it should function similarly to fetch.
import wretch from 'wretch';
/**
* Handles Server Error
*
* #param {object} err HTTP Error
*
* #return {undefined} Returns undefined
*/
function handleServerError(err) {
console.error(err);
}
const api = wretch()
.options({ credentials: 'include', mode: 'cors' })
.url(window.appBaseUrl || process.env.REACT_APP_API_HOST_NAME)
.resolve(_ => _.error(handleServerError))
export default api;
Attempt:
I've tried using wretch's .signal() parameter with an AbortController(), calling .abort() after the request, but that aborts all requests, causing my app to break. Example below:
import wretch from 'wretch';
/**
* Handles Server Error
*
* #param {object} err HTTP Error
*
* #return {undefined} Returns undefined
*/
function handleServerError(err) {
console.error(err);
}
const controller = new AbortController();
const api = wretch()
.signal(controller)
.options({ credentials: 'include', mode: 'cors' })
.url(window.appBaseUrl || process.env.REACT_APP_API_HOST_NAME)
.resolve(_ => _.error(handleServerError))
controller.abort();
export default api;
I've tried moving the logic around to various places, but it seems abort all actions or abort none of them.
Any advice as to how to go about this would be appreciated, this is critical for my team.
Thank you
I feel pretty silly right now, but this is what it took to get it working.
Solution Steps:
Set an AbortController to the initialState of the reducer
reducer.js
export default (state = {
controller: new AbortController(),
}, action) => {
switch (action.type) {
...
Get the AbortController from the state, at the beginning of the fetch action and abort it.
Create a new AbortController and pass it into the requestData action.
Pass the new AbortController into the signal() param of the wretch call.
actions.js
export function fetchData(params) {
return (dispatch, getState) => {
const { controller } = getState().reducer;
controller.abort();
const newController = new AbortController();
dispatch(requestData(newController));
return api
.signal(newController)
.query(params)
.url('api/data')
.get()
.fetchError(err => {
console.log(err);
dispatch(apiFail(err.toString()));
})
.json(response => dispatch(receiveData(response.items, response.totalItems)))
}
}
export function requestData(controller) {
return {
type: REQUEST_DATA,
waiting: true,
controller,
}
}
In the reducer, for the case of the requestData action, set the new AbortController to the state.
reducer.js
case REQUEST_DATA:
return {
...state,
waiting: action.waiting,
controller: action.controller
};
There's some additional functionality with wretch, an .onAbort() param, that allows you to dispatch other actions when the request is aborted. I haven't coded that out yet, but I figured I'd include the info for anyone else struggling with this.
I'm new to react / redux
When I doing the project in jquery
I will make some functions like this:
errorHandle (code) {
if(code = 1){
$('.popUpA').show()
}
...
}
callAPI (){
//Do AJAX call
//If Error , call errorHandle()
}
In the new project,
I use the axios to call API in the api helper
export function getDataList(){
//axios....
}
export function getData(){
//axios....
}
And I use the Store to trigger the show/hide popUp , I will use dispatch(showPopup()) and dispatch(showPopup(hide)) in component
But I want that if api function have error , will pass the response in to the errorHandler , then dispatch the showPopup. I don't have idea how to add this into the exported function.
Any suggestion or example?
this is my abstraction of axios request, I use it in services that are used with Redux:
import axios from 'axios';
import { API } from '../../constants';
import { revokeAuthAction } from ;
export const getAuth = () => {
// Auth logic
};
/**
* Create an Axios Client with defaults
*/
const client = axios.create({
baseURL: API.BASEURL,
headers: {
Authorization: getAuth(),
'Access-Control-Max-Age': 1728000,
// 'X-Authorization-JWT':
},
});
/**
* Request Wrapper with default success/error actions
*/
const request = (options) => {
const onSuccess = (response) => options.raw ? response : response.data;
// console.debug('Request Successful!', response);
// If options.raw is true, return all response
const onError = (error) => {
// console.error('Request Failed:', error.config);
if (error.response) {
if (error.response.status === 401) {
// console.error('Unauthorized');
store.dispatch(revokeAuthAction());
} else {
// Request was made but server responded with something
// other than 2xx
// console.error('Status:', error.response.status);
// console.error('Data:', error.response.data);
// console.error('Headers:', error.response.headers);
}
} else {
// Something else happened while setting up the request
// triggered the error
// console.error('Error Message:', error.message);
}
return Promise.reject(error.response || error.message);
};
return client(options)
.then(onSuccess)
.catch(onError); // in realtà non catcho un bel niente perchè ritorno Promise.reject e quindi il giro ricomincia
};
export default request;
There are more library to handle redux async action calls. I use redux-thunk another well known library is redux-saga. With redux thunk you will add a middleware to redux and this way you can create async action creators which return a function, and they can call other action creators depending of the result of the async call.
You add the middleware this way:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux#>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
And your action creator will be something like this:
export function requestDataList() {
return function (dispatch: Function, getState: Function) {
return getDataList().then(resp => {
dispatch(dataListReceived(resp.data));
}).catch(error => {
dispatch(showPopup(title, error.message));
});
};
}
So if your getDataList retrun an axios promise, on success it will call an action wit the result. On error it can call the error dialog.
How do I send custom parameters to the axios interceptor? I am using an interceptor like this:
window.axios.interceptors.request.use(function (config) {
if (PASSED_PARAM == true) {
doSomethingAwesome();
}
return config;
}, function (error) {
return Promise.reject(error);
});
I also have a response interceptor that needs to receive the same parameter.
Merge params
axios.interceptors.request.use((config) => {
config.params = {...config.params, my_variable: 'value'}
return config
})
The method suggested by #Laurent will cause axios to wipe out all your other parameters and replace it with my_variable, which is may not exactly what you want.
The proper way of adding default parameters instead of replacing it is like this:
axios.defaults.params = {};
axios.interceptors.request.use(function (config) {
config.params['blah-defaut-param'] = 'blah-blah-default-value';
return config;
}, function (error) {
return Promise.reject(error);
});
This works with axios 0.18.1. It does not work with axios 0.19 due to a regression bug..., I believe it will be fixed soon.
The accepted answer, and the answers on this page seem to have missed what the question is asking.
This question is asking something like "When I call axios can I pass data to the interceptor but not to the server" and the answer is yes. Though it is undocumented and when using typescript you'll have to add a //#ts-ignore.
When you call axios you can pass a config object, (either after the url, or if you're not using a shortcut method like .get/.post the axios function just takes a config object. The good news is that config object is always passed along with the response so you can get to it in the interceptor and in the promise handers.
its available on the response objects as response.config and on the error as error.response.config
//#ts-ignore -- typescript will complain that your param isn't expected on the config object.
axios({
method: "get",
url: '/myapi/gocrazy',
// just piggyback any data you want to the end of config, just don't
// use any key's that axios is already expecting
PASSED_PARAM: true
}
//in the interceptor, config is attached to the object, and it keeps any extra keys you tacked on the end.
window.axios.interceptors.request.use(function (config) {
if (config.PASSED_PARAM == true) {
doSomethingAwesome();
}
return config;
}, function (error) {
return Promise.reject(error);
});
window.axios.interceptors.response.use(function (response) {
if (response.config.PASSED_PARAM == true) {
doSomethingAwesome();
}
return response;
}, function (error) {
if (error.response.config.PASSED_PARAM == true) {
doSomethingElseAwesome();
}
return Promise.reject(error);
});
Working solution
It's actually fairly simple to add parameters to the query with Axios interceptors when you send data.
axios.interceptors.request.use((config) => {
config.params = {my_variable: 'value'}
return config
})
axios allows to pass some additional request parameters:
axios.post('/api', `some body`,
{headers: {'Content-Type': ' text/html;charset=UTF-8'},
param: true});
and interceptor:
this.axios.interceptors.request.use(req => {
console.log(`${req.method}: ${req.param}`); //output: `/api: true`
return req;
});
I have tested it on version: 0.21.1
I ended up using the headers object. Not sure if that is recommended, or if it's anti-pattern. But anyhow it works. I am not entirely sure about how many bytes the header adds to the server request head, but I think it's neglectable.
In addition to the answer from DiamondDrake, the global typings can be overridden to not have to 'ts-ignore' the usage:
declare module 'axios' {
interface AxiosRequestConfig extends OriginalAxiosRequestConfig {
PASSED_PARAM: boolean;
}
}
You cannot pass param but you can update the passed param config. request interceptor logic runs before executing requests. It is kinda middlewares So maybe you need to access to tokens and update the request headers
axios.interceptors.request.use(
(config) => {
// or maybe you need to read the stored cookies
const user = localStorage.getItem("user");
if (user) {
// If user exists get the token
const token = JSON.parse(user).token;
// and then update the headers
config.headers.Authorization = `Bearer ${token}`;
}
// maybe u need to refresh the access tokens, you do it in interceptor
return config;
},
(err) => {
return Promise.reject(err);
}
);
The answer of https://stackoverflow.com/users/12706095/zack is kinda correct.
You should create an axios.d.ts file including the following lines, the rest is done by TypeScript.
import "axios";
declare module "axios" {
export interface AxiosRequestConfig {
/** A custom axios request config param that can be used anywhere now. */
myParam?: boolean;
}
}
Now TypeScript won't bother you anymore when you want to use this custom property anywhere when accessing the AxioRequestConfig, e.g. your interceptor.
See Axios typescript customize AxiosRequestConfig for more info.
I am trying to make my Vue app have server-side rendering. I am using vue-server-renderer (https://www.npmjs.com/package/vue-server-renderer). Client-side rendering is working fine.
My app use vue-router and axios
Here is my server.js:
server.get('*', (request, response) => {
bundleRenderer.renderToString({ url: request.url }, (error, htmlPromise) => {
if (error) {
// Log the error in the console
console.error(error)
// Tell the client something went wrong
return response
.status(500)
.send(error)
}
response.send(layout.replace('<div id=app></div>', htmlPromise))
})
})
getInfo() is the method to fetch server data.
Here is getInfo():
export default {
methods: {
getInfo(api) {
return axios
.get(api || this.$route.params.path)
.then((data) => {
this.data = data
this.$set(this, 'isLoading', false)
})
},
},
}
My server entry is:
import { app, router, store } from './index'
export default context => {
let componentPromises = router.getMatchedComponents().filter((component) => {
return component.methods && component.methods.getInfo
}).map((component) => {
return component.methods.getInfo()
})
return Promise.all(componentPromises).then(() => {
return app
})
}
However, I soon realize that all the components from router.getMatchedComponents() does not have $route or $set. Therefore, the method getInfo() stops working.
The document from https://router.vuejs.org/en/api/router-instance.html is very short and does not provide much information:
router.getMatchedComponents()
Returns an Array of the components (definition/constructor, not
instances) matched by the current route. This is mostly used during
server-side rendering to perform data prefetching.
How can I fix the problem?
I have previously incurred into a similar problem and managed to successfully prefetch data by doing the following:
app.$router.onReady(() => {
const matchedComponents = app.$router.getMatchedComponents()
if (!matchedComponents.length) { /* ... */}
Promise.all(matchedComponents.map((Component: any) => {
if (Component.options.methods.asyncData) {
return Component.options.methods.asyncData({
store: app.$store,
route: app.$router.currentRoute
});
}
})).then(() => { /* your callback here ... */ });
}
According to vue ssr documentation (https://ssr.vuejs.org/en/data.html) the suggested way is to use a custom asyncData method in your component to perform data fetching rather than calling component methods directly:
export default {
asyncData ({ store, route }) {
// return the Promise from the action
return store.dispatch('fetchItem', route.params.id)
}
},