Handling Axios error in React - reactjs

I have a React component that calls a function getAllPeople:
componentDidMount() {
getAllPeople().then(response => {
this.setState(() => ({ people: response.data }));
});
}
getAllPeople is in my api module:
export function getAllPeople() {
return axios
.get("/api/getAllPeople")
.then(response => {
return response.data;
})
.catch(error => {
return error;
});
}
I think this is a very basic question, but assuming I want to handle the error in my root component (in my componentDidMount method), not in the api function, how does this root component know whether or not I the axios call returns an error? I.e. what is the best way to handle errors coming from an axios promise?

Better way to handle API error with Promise catch method*.
axios.get(people)
.then((response) => {
// Success
})
.catch((error) => {
// Error
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
// console.log(error.response.data);
// console.log(error.response.status);
// console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the
// browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
});

The getAllPeople function already returns the data or error message from your axios call. So, in componentDidMount, you need to check the return value of your call to getAllPeople to decide whether it was an error or valid data that was returned.
componentDidMount() {
getAllPeople().then(response => {
if(response!=error) //error is the error object you can get from the axios call
this.setState(() => ({ people: response}));
else { // your error handling goes here
}
});
}
If you want to return a promise from your api, you should not resolve the promise returned by your axios call in the api. Instead you can do the following:
export function getAllPeople() {
return axios.get("/api/getAllPeople");
}
Then you can resolve in componentDidMount.
componentDidMount() {
getAllPeople()
.then(response => {
this.setState(() => ({ people: response.data}));
})
.catch(error => {
// your error handling goes here
}
}

My suggestion is to use a cutting-edge feature of React. Error Boundaries
This is an example of using this feature by Dan Abramov.
In this case, you can wrap your component with this Error Boundary component.
What is special for catching the error in axios is that you can use
interceptors for catching API errors.
Your Error Boundary component might look like
import React, { Component } from 'react';
const errorHandler = (WrappedComponent, axios) => {
return class EH extends Component {
state = {
error: null
};
componentDidMount() {
// Set axios interceptors
this.requestInterceptor = axios.interceptors.request.use(req => {
this.setState({ error: null });
return req;
});
this.responseInterceptor = axios.interceptors.response.use(
res => res,
error => {
alert('Error happened');
this.setState({ error });
}
);
}
componentWillUnmount() {
// Remove handlers, so Garbage Collector will get rid of if WrappedComponent will be removed
axios.interceptors.request.eject(this.requestInterceptor);
axios.interceptors.response.eject(this.responseInterceptor);
}
render() {
let renderSection = this.state.error ? <div>Error</div> : <WrappedComponent {...this.props} />
return renderSection;
}
};
};
export default errorHandler;
Then, you can wrap your root component passing axios instance with it
errorHandler(Checkout, api)
As a result, you don't need to think about error inside your component at all.

You could check the response before setting it to state. Something like
componentDidMount() {
getAllPeople().then(response => {
// check if its actual response or error
if(error) this.setState(() => ({ error: response }));
else this.setState(() => ({ people: response}));
});
}
Its relying on the fact that axios will return different objects for success and failures.

The solution from Yevhenii Herasymchuk was very close to what I needed however, I aimed for an implementation with functional components so that I could use Hooks and Redux.
First I created a wrapper:
export const http = Axios.create({
baseURL: "/api",
timeout: 30000,
});
function ErrorHandler(props) {
useEffect(() => {
//Request interceptor
http.interceptors.request.use(function (request) {
// Do something here with Hooks or something else
return request;
});
//Response interceptor
http.interceptors.response.use(function (response) {
if (response.status === 400) {
// Do something here with Hooks or something else
return response;
}
return response;
});
}, []);
return props.children;
}
export default ErrorHandler;
Then I wrapped the part of the project that I needed to check how axios behaved.
<ErrorHandler>
<MainPage/>
</ErrorHandler>
Lastly, I import the axios instance(http) wherever I need it in the project.
Hope it helps anyone that wishes for a different approach.

I just combined both answers by Yevhenii Herasymchuk and GeorgeCodeHub, fixed some mistakes and ported it into React hooks. So here is my final working version:
// [project-directory]/src/lib/axios.js
import Axios from 'axios';
const axios = Axios.create({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
withCredentials: true,
});
export default axios;
// [project-directory]/src/components/AxiosErrorHandler.js
import {useEffect} from 'react';
import axios from '#/lib/axios';
const AxiosErrorHandler = ({children}) => {
useEffect(() => {
// Request interceptor
const requestInterceptor = axios.interceptors.request.use((request) => {
// Do something here with request if you need to
return request;
});
// Response interceptor
const responseInterceptor = axios.interceptors.response.use((response) => {
// Handle response here
return response;
}, (error) => {
// Handle errors here
if (error.response?.status) {
switch (error.response.status) {
case 401:
// Handle Unauthenticated here
break;
case 403:
// Handle Unauthorized here
break;
// ... And so on
}
}
return error;
});
return () => {
// Remove handlers here
axios.interceptors.request.eject(requestInterceptor);
axios.interceptors.response.eject(responseInterceptor);
};
}, []);
return children;
};
export default AxiosErrorHandler;
Usage:
// Wrap it around your Layout or any component that you want
return (
<AxiosErrorHandler>
<div>Hello from my layout</div>
</AxiosErrorHandler>
);

Related

Writing integration test for asynchronous code in React application with Context and useEffect

So, in useEffect I am fetching an object from the API then I am dispatching response data to the Context reducer and then updating the state. It looks something like this:
export const fetchItem = (id) => request({url: `/items/${id}`, method: 'get'});
...
const {dispatch, singleItem} = useProvider();
useEffect(() => {
const id = getItemIdFromUrl(props);
fetchItem(id).then((response) => {
dispatch(action(response.data.data));
});
}, [props, dispatch]);
I would like to write a good integration test for this. I am using react-testing-library with Jest. I am trying to mock the return value of the fetchItem function and then to check if everything is rendered correctly but constantly getting this warning:
act(() => {
/* fire events that update state */
});
/* assert on the output */
Any chance to do this correctly?
This is how the request method looks like:
import axios from 'axios';
import humps from 'humps';
import {getItem} from './localStorage';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
});
api.interceptors.response.use(
(response) => humps.camelizeKeys(response),
(error) => Promise.reject(error.response),
);
api.interceptors.request.use(
(request) => {
request.data = humps.decamelizeKeys(request.data);
return request;
},
(error) => Promise.reject(error.request),
);
export default function request({url, method, headers = {}, data}) {
try {
const token = getItem('token');
headers.Authorization = token;
return api({method, url, headers, data});
} catch (error) {
if (error.status === 500) {
console.log('HANDLE ERROR: ', error);
}
throw error;
}
}

React and promise issues with fetch method

i'm new in react and i've got some issues with asynchronous fetch data :
i want to fetch github users
function fetchUser(username) {
return fetch(`https://api.github.com/users/${username}`)
.then(response => response.json())
.then(data => data)
}
export function getUserData(username) {
const object = {
profile: fetchUser(username),
}
console.log(object)
return object
}
and this is my method in my component
componentDidMount() {
getUserData(this.props.playerOne)
}
but this is what i got in my console
{profile: Promise}
i'm sure that i dont understand well this promise so could you help me to have not a Promise in my object but the data i'm fetching ? (if i log data in my fetch i got what i want)
You can make this function async and wait for the promise resolution.
export async function getUserData(username) {
const object = {
profile: await fetchUser(username),
}
console.log(object)
return object
}
then in your componentDidMount
componentDidMount() {
getUserData(this.props.playerOne)
.then((profile) => { this.setState({ profile }) })
}

How to make API requests correctly with Reactjs and Axios?

I am using the latest version of ReactJS and making requests with Axios. But before I entered the cancel() function I was getting the following error:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
in MenuPlaylist (at Sidebar/index.js:19)
in aside (created by Context.Consumer)
in StyledComponent (created by styled.aside)
in styled.aside (at Sidebar/index.js:11)
in Sidebar (at Search/index.js:16)
in Search (created by Context.Consumer)
Because the problem was as soon as the component disassembled it leaked the memory. But now canceling the request I get the following messages on the console:
And I find it quite strange, as it seems that the system is in trouble.
What is the correct way to cancel requests to avoid memory leaks?
COMPONENT:
import React, { Component } from "react";
// STYLES
import { Menu, Title } from "./styles";
// SERVICES
import { cancelAxiosRequest, getAllPlaylist } from "services/Api";
// SUBCOMPONENT'S
import { CreatePlaylist, CreatedPlaylist } from "components";
class MenuPlaylist extends Component {
state = {
data: []
};
// LIFE CYCLES
componentDidMount() {
this.consumeAPI();
}
componentWillUnmount() {
cancelAxiosRequest("Request Canceled.");
}
// METHODS
consumeAPI = () => {
getAllPlaylist().then(({ data }) => {
this.setState({ data: data });
});
};
render = () => {
return (
<Menu>
<Title>PLAYLISTS</Title>
<CreatePlaylist />
<CreatedPlaylist data={this.state.data} />
</Menu>
);
};
}
export default MenuPlaylist;
AXIOS:
import axios from "axios";
const instance = axios.create({
baseURL: "http://localhost:3001",
timeout: 1000
});
let CancelToken = axios.CancelToken;
export let cancelAxiosRequest;
// GET'S
export function getNewReleases() {
return instance.get("/newReleases");
}
export function getAllPlaylist() {
return instance.get("/playlist", {
cancelToken: new CancelToken(function executor(c) {
cancelAxiosRequest = c;
})
});
}
export function getPlaylist(name) {
return instance.get("/playlist", {
params: {
name: name
}
});
}
// POST'S
export function postNewPlaylist(id, name) {
return instance.post("/playlist", {
id: id,
to: `/playlist/${id}`,
name: name
});
}
According to the documentation "cancelling" means the promise throws a Cancel error with the message you specify.
to fix:
// ...
getAllPlaylist()
.then(({ data }) => {
this.setState({ data });
})
.catch(err => {
if (axios.isCancel(err)) {
return; // ignore
}
throw err; // or handle other errors
});
// ...
Though I feel changing the value of an export is not really the best way to use cancel tokens. Check if you need to be unmounting the component in the first place, but if it truly is just going to be unmounted sometimes, maybe try something like
export function getAllPlaylist(cancelToken) {
return instance.get("/playlist", {
cancelToken: cancelToken
});
}
// ...
getAllPlaylist(new CancelToken(c => this.cancel = c))
.then(({ data }) => {
this.setState({ data });
});
// ...
componentWillUnmount() {
this.cancel();
}
I don't know if this is 100% ideal, but this avoids multiple instances of the component fighting over a single variable.
in the catch you should do something like
.catch((e) => {
if (axios.isCancel(e)) return;
setError(true);
});
because canceling isn't an error

Axios middleware to use in all instances of axios

I'm using axios in my react app using import axios from 'axios in many of my scripts. I want to use sort of a middleware that is invoked for all axios calls/errors. How do I approach this?
As per the documentation - You need to create a file i.e
// api-client.js
import axios from 'axios';
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
console.log(config);
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);
});
export default axios;
Then from your container or controller, import above file:
// Home.js
import apiClient from './api-client.js';
Interceptors are the Axios way of doing this. For me though, it was too limited, tangled in Axios' API, difficult to test, etc.
Axios-middleware
So I wrote the axios-middleware module, a simple middleware service that hooks itself in your axios instance (either global or a local one) and provides a simple, self-contained and easily testable middleware API.
Note: it shines in bigger apps where minimal coupling is really important.
Simple example
Here's a simple example from the documentation
import axios from 'axios';
import { Service } from 'axios-middleware';
const service = new Service(axios);
service.register({
onRequest(config) {
console.log('onRequest');
return config;
},
onSync(promise) {
console.log('onSync');
return promise;
},
onResponse(response) {
console.log('onResponse');
return response;
}
});
console.log('Ready to fetch.');
// Just use axios like you would normally.
axios('https://jsonplaceholder.typicode.com/posts/1')
.then(({ data }) => console.log('Received:', data));
It should output:
Ready to fetch.
onRequest
onSync
onResponse
Received: {userId: 1, id: 1, title: ...
Testing a middleware
Say we have the following self-contained middleware class that we want to test.
export default class ApiErrorMiddleware {
constructor(toast) {
this.toast = toast;
}
onResponseError(err = {}) {
let errorKey = 'errors.default';
const { response } = err;
if (response && response.status) {
errorKey = `errors.${response.status}`;
} else if (err.message === 'Network Error') {
errorKey = 'errors.network-error';
}
this.toast.error(errorKey);
throw err;
}
}
Then it's really easy, we don't even need to mock Axios.
import ApiErrorMiddleware from '#/middlewares/ApiErrorMiddleware';
describe('ApiErrorMiddleware', () => {
let toast;
let middleware;
// Jest needs a function when we're expecting an error to be thrown.
function onResponseError(err) {
return () => middleware.onResponseError(err);
}
beforeEach(() => {
toast = { error: jest.fn() };
middleware = new ApiErrorMiddleware(toast);
});
it('sends a code error message', () => {
expect(onResponseError({ response: { status: 404 } })).toThrow();
expect(toast.error).toHaveBeenLastCalledWith('errors.404');
});
});

how to cancel/abort ajax request in axios

I use axios for ajax requests and reactJS + flux for render UI. In my app there is third side timeline (reactJS component). Timeline can be managed by mouse's scroll. App sends ajax request for the actual data after any scroll event. Problem that processing of request at server can be more slow than next scroll event. In this case app can have several (2-3 usually) requests that already is deprecated because user scrolls further. it is a problem because every time at receiving of new data timeline begins redraw. (Because it's reactJS + flux) Because of this, the user sees the movement of the timeline back and forth several times. The easiest way to solve this problem, it just abort previous ajax request as in jQuery. For example:
$(document).ready(
var xhr;
var fn = function(){
if(xhr && xhr.readyState != 4){
xhr.abort();
}
xhr = $.ajax({
url: 'ajax/progress.ftl',
success: function(data) {
//do something
}
});
};
var interval = setInterval(fn, 500);
);
How to cancel/abort requests in axios?
Axios does not support canceling requests at the moment. Please see this issue for details.
UPDATE: Cancellation support was added in axios v0.15.
EDIT: The axios cancel token API is based on the withdrawn cancelable promises proposal.
UPDATE 2022: Starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
Example:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
Using useEffect hook:
useEffect(() => {
const ourRequest = Axios.CancelToken.source() // <-- 1st step
const fetchPost = async () => {
try {
const response = await Axios.get(`endpointURL`, {
cancelToken: ourRequest.token, // <-- 2nd step
})
console.log(response.data)
setPost(response.data)
setIsLoading(false)
} catch (err) {
console.log('There was a problem or request was cancelled.')
}
}
fetchPost()
return () => {
ourRequest.cancel() // <-- 3rd step
}
}, [])
Note: For POST request, pass cancelToken as 3rd argument
Axios.post(`endpointURL`, {data}, {
cancelToken: ourRequest.token, // 2nd step
})
Typically you want to cancel the previous ajax request and ignore it's coming response, only when a new ajax request of that instance is started, for this purpose, do the following:
Example: getting some comments from API:
// declare an ajax request's cancelToken (globally)
let ajaxRequest = null;
function getComments() {
// cancel previous ajax if exists
if (ajaxRequest ) {
ajaxRequest.cancel();
}
// creates a new token for upcomming ajax (overwrite the previous one)
ajaxRequest = axios.CancelToken.source();
return axios.get('/api/get-comments', { cancelToken: ajaxRequest.token }).then((response) => {
console.log(response.data)
}).catch(function(err) {
if (axios.isCancel(err)) {
console.log('Previous request canceled, new request is send', err.message);
} else {
// handle error
}
});
}
import React, { Component } from "react";
import axios from "axios";
const CancelToken = axios.CancelToken;
let cancel;
class Abc extends Component {
componentDidMount() {
this.Api();
}
Api() {
// Cancel previous request
if (cancel !== undefined) {
cancel();
}
axios.post(URL, reqBody, {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
}),
})
.then((response) => {
//responce Body
})
.catch((error) => {
if (axios.isCancel(error)) {
console.log("post Request canceled");
}
});
}
render() {
return <h2>cancel Axios Request</h2>;
}
}
export default Abc;
There is really nice package with few examples of usage called axios-cancel.
I've found it very helpful.
Here is the link: https://www.npmjs.com/package/axios-cancel
https://github.com/axios/axios#cancellation
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
let url = 'www.url.com'
axios.get(url, {
progress: false,
cancelToken: source.token
})
.then(resp => {
alert('done')
})
setTimeout(() => {
source.cancel('Operation canceled by the user.');
},'1000')
This is how I did it using promises in node. Pollings stop after making the first request.
var axios = require('axios');
var CancelToken = axios.CancelToken;
var cancel;
axios.get('www.url.com',
{
cancelToken: new CancelToken(
function executor(c) {
cancel = c;
})
}
).then((response) =>{
cancel();
})
Using cp-axios wrapper you able to abort your requests with three diffent types of the cancellation API:
1. Promise cancallation API (CPromise):
Live browser example
const cpAxios= require('cp-axios');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const chain = cpAxios(url)
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
chain.cancel();
}, 500);
2. Using AbortController signal API:
const cpAxios= require('cp-axios');
const CPromise= require('c-promise2');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const abortController = new CPromise.AbortController();
const {signal} = abortController;
const chain = cpAxios(url, {signal})
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
abortController.abort();
}, 500);
3. Using a plain axios cancelToken:
const cpAxios= require('cp-axios');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const source = cpAxios.CancelToken.source();
cpAxios(url, {cancelToken: source.token})
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
source.cancel();
}, 500);
4. Usage in a custom React hook (Live Demo):
import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
/*
Note: the related network request will be aborted as well
Check out your network console
*/
function TestComponent({ url, timeout }) {
const [cancel, done, result, err] = useAsyncEffect(
function* () {
return (yield cpAxios(url).timeout(timeout)).data;
},
{ states: true, deps: [url] }
);
return (
<div>
{done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
<button onClick={cancel} disabled={done}>
Cancel async effect (abort request)
</button>
</div>
);
}
Update
Axios v0.22.0+ supports AbortController natively:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
Starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
CancelToken deprecated
You can also cancel a request using a CancelToken.
The axios cancel token API is based on the withdrawn cancelable promises proposal.
This API is deprecated since v0.22.0 and shouldn't be used in new projects
You can create a cancel token using the CancelToken.source factory as shown below:
import {useState, useEffect} from 'react'
export function useProfileInformation({accessToken}) {
const [profileInfo, setProfileInfo] = useState(null)
useEffect(() => {
const abortController = new AbortController()
window
.fetch('https://api.example.com/v1/me', {
headers: {Authorization: `Bearer ${accessToken}`},
method: 'GET',
mode: 'cors',
signal: abortController.signal,
})
.then(res => res.json())
.then(res => setProfileInfo(res.profileInfo))
return function cancel() {
abortController.abort()
}
}, [accessToken])
return profileInfo
}
// src/app.jsx
import React from 'react'
import {useProfileInformation} from './hooks/useProfileInformation'
export function App({accessToken}) {
try {
const profileInfo = useProfileInformation({accessToken})
if (profileInfo) {
return <h1>Hey, ${profileInfo.name}!</h1>
} else {
return <h1>Loading Profile Information</h1>
}
} catch (err) {
return <h1>Failed to load profile. Error: {err.message}</h1>
}
}

Resources