Cancel Axios request in useEffect not running - reactjs

I am attempting to cancel an axios request, but I am only having partial success. I have this function that returns a Promise when I make a GET request:
const getCards = (token) => {
const URL = isDev
? "https://cors-anywhere.herokuapp.com/https://privacy.com/api/v1/card"
: "https://privacy.com/api/v1/card";
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
cancelToken: source.token,
};
return axios.get(URL, config);
};
I call this function inside updateCards() which looks like this:
const updateCards = async () => {
console.log("Updating Cards");
setCards([]);
setStarted(true);
setLoading(true);
let response = await getCards(token).catch((thrown) => {
if (axios.isCancel(thrown)) {
console.error("[UpdateCards]", thrown.message);
}
});
/**
* response is undefined when we cancel the request on unmount
*/
if (typeof response === "undefined") return console.log("Undefined");
console.log("Got Response from UpdateCards", response);
setLoading(false);
};
I cancel my request in the useEffect hook as so:
useEffect(() => {
return () => {
source.cancel()
}
}, [])
And I have setup my CancelToken under my state declaration like this:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
My issue is that, if I call my updateCards() function inside of the useEffect(), I can cancel it just fine, but if I use a button to call that same function the cancel is not ran. I have looked everywhere and the only solution I have found is that I have to call my requests within the useEffect() hook, but that is not something I want to do.Where do I go from here?
Here are the resources I have looked at:
https://github.com/axios/axios#cancellation
https://medium.com/#selvaganesh93/how-to-clean-up-subscriptions-in-react-components-using-abortcontroller-72335f19b6f7
Cant cancel Axios post request via CancelToken

To have a place to store a variable that behaves like an instance variable of the component, you can use useRef. It's a container for anything you want. You can store the CancelToken in there:
function Privacy(props) {
const source = useRef(null);
function getSource() {
if (source.current == null) {
const CancelToken = axios.CancelToken;
source.current = CancelToken.source();
}
return source.current;
}
useEffect(() => {
return () => {
if (source.current != null) source.current.cancel();
}
}, [])
// call `getSource()` wherever you need the Axios source
}

Related

React & Fetch async/await... Why I'm receiving a promise?

I'm having some troubles about react, fetch and async/await.
I have this code as a fetch wrapper:
export const fetcher = (url, options = null) => {
const handle401Response = error => {
throw error;
}
const isResponseValid = response => {
if (!response.ok)
throw response;
else
return response.json();
}
return fetch(url, { ...options, credentials: 'include' })
.then(isResponseValid)
.catch(handle401Response);
}
Then I define some API calls functions like:
export const getGroups = (id = null) => {
return fetcher(`${API_GROUP_URL}${id !== null ? `?id=${id}` : ''}`);
}
And then I try to use it like:
export function SomeComponent(props) {
const groups = async () => {
try {
const ret = await getGroups();
return ret;
} catch (err) {
console.log(err);
}
};
console.log(groups());
return <h1>Component</h1>
}
The result in console is: Promise{}.
I have read docs about async/await but can't understand why await is not waiting for promise to end.
Thanks in advance!
export function SomeComponent(props) {
const [data, setData] = useState()
const groups = async () => {
};
useEffect(() => {
const fetchData = async () => {
try {
const ret = await getGroups();
// process and set data accordingly
setData(ret)
} catch (err) {
console.log(err);
}
}
// fetch data inside useEffect
fetchData()
}, [])
// console.log(groups());
return <h1>Component {data?.prop}</h1>
}
Hope this gives you an idea on how to fetch in a functional component
Async functions always return a promise. The time when you call that function it will give you back a promise instantly. You have used await inside the function and it is waiting for getGroup promise.
In normal javascript function console.log(await) this will fix the issue but in react you have to do it inside a another function because you cant make react components async (at least not in React 17 and below)

Axios throwing CanceledError with Abort controller in react

I have built an axios private instance with interceptors to manage auth request.
The system has a custom axios instance:
const BASE_URL = 'http://localhost:8000';
export const axiosPrivate = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
A custom useRefreshToken hook returns accessToken using the refresh token:
const useRefreshToken = () => {
const { setAuth } = useAuth();
const refresh = async () => {
const response = await refreshTokens();
// console.log('response', response);
const { user, roles, accessToken } = response.data;
setAuth({ user, roles, accessToken });
// return accessToken for use in axiosClient
return accessToken;
};
return refresh;
};
export default useRefreshToken;
Axios interceptors are attached to this axios instance in useAxiosPrivate.js file to attached accessToken to request and refresh the accessToken using a refresh token if expired.
const useAxiosPrivate = () => {
const { auth } = useAuth();
const refresh = useRefreshToken();
useEffect(() => {
const requestIntercept = axiosPrivate.interceptors.request.use(
(config) => {
// attach the access token to the request if missing
if (!config.headers['Authorization']) {
config.headers['Authorization'] = `Bearer ${auth?.accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
const responseIntercept = axiosPrivate.interceptors.response.use(
(response) => response,
async (error) => {
const prevRequest = error?.config;
// sent = custom property, after 1st request - sent = true, so no looping requests
if (error?.response?.status === 403 && !prevRequest?.sent) {
prevRequest.sent = true;
const newAccessToken = await refresh();
prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequest);
}
return Promise.reject(error);
}
);
// remove the interceptor when the component unmounts
return () => {
axiosPrivate.interceptors.response.eject(responseIntercept);
axiosPrivate.interceptors.request.eject(requestIntercept);
};
}, [auth, refresh]);
return axiosPrivate;
};
export default useAxiosPrivate;
Now, this private axios instance is called in functional component - PanelLayout which is used to wrap around the pages and provide layout.
Here, I've tried to use AbortControllers in axios to terminate the request after the component is mounted.
function PanelLayout({ children, title }) {
const [user, setUser] = useState(null);
const axiosPrivate = useAxiosPrivate();
const router = useRouter();
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const signal = controller.signal;
const getUserProfile = async () => {
try {
const response = await axiosPrivate.get('/api/identity/profile', {
signal,
});
console.log(response.data);
isMounted && setUser(response.data.user);
} catch (error) {
console.log(error);
router.push({
pathname: '/seller/auth/login',
query: { from: router.pathname },
});
}
};
getUserProfile();
return () => {
isMounted = false;
controller.abort();
};
}, []);
console.log('page rendered');
return (
<div className='flex items-start'>
<Sidebar className='h-screen w-[10rem]' />
<section className='min-h-screen flex flex-col'>
<PanelHeader title={title} classname='left-[10rem] h-[3.5rem]' />
<main className='mt-[3.5rem] flex-1'>{children}</main>
</section>
</div>
);
}
export default PanelLayout;
However, the above code is throwing the following error:
CanceledError {message: 'canceled', name: 'CanceledError', code: 'ERR_CANCELED'}
code: "ERR_CANCELED"
message: "canceled"
name: "CanceledError"
[[Prototype]]: AxiosError
constructor: ƒ CanceledError(message)
__CANCEL__: true
[[Prototype]]: Error
Please suggest how to avoid the above error and get axios to work properly.
I also encountered the same issue and I thought that there was some flaw in my logic which caused the component to be mounted twice. After doing some digging I found that react apparently added this feature with with the new version 18 in StrictMode where useEffect was being run twice. Here's a link to the article clearly explaining this new behaviour.
One way you could solve this problem is by removing StrictMode from your application (Temporary Solution)
Another way is by using useRef hook to store some piece of state which is updated when your application is mounted the second time.
// CODE BEFORE USE EFFECT
const effectRun = useRef(false);
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const signal = controller.signal;
const getUserProfile = async () => {
try {
const response = await axiosPrivate.get('/api/identity/profile', {
signal,
});
console.log(response.data);
isMounted && setUser(response.data.user);
} catch (error) {
console.log(error);
router.push({
pathname: '/seller/auth/login',
query: { from: router.pathname },
});
}
};
// Check if useEffect has run the first time
if (effectRun.current) {
getUserProfile();
}
return () => {
isMounted = false;
controller.abort();
effectRun.current = true; // update the value of effectRun to true
};
}, []);
// CODE AFTER USE EFFECT
Found the solution from this YouTube video.
I, too, encountered this issue. What made it worse is that axios doesn't provide an HTTP status code when the request has been canceled, although you do get error.code === "ERR_CANCELED". I solved it by handling the abort within the axios interceptor:
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.code === "ERR_CANCELED") {
// aborted in useEffect cleanup
return Promise.resolve({status: 499})
}
return Promise.reject((error.response && error.response.data) || 'Error')
}
);
As you can see, I ensure that the error response in the case of an abort supplies a status code of 499.
I faced the same problem in similar project, lets start by understanding first the root cause of that problem.
in react 18 the try to make us convenient to the idea of mounting and unmounting components twice for future features that the are preparing, the the useEffect hook now is mounted first time then unmounted the mounted finally.
so they need from us adapt our projects to the idea of mount and unmount of components twice
so you have two ways, adapting these changes and try to adapt your code to accept mounting twice, or making some turn around code to overcome mounting twice, and I would prefer the first one.
here in your code after first mount you aborted your API request in clean up function, so when the component dismount and remount again it face an error when try to run previously aborted request, so it throw exception, that's what happens
1st solution (adapting to react changing):
return () => {
isMounted = false
isMounted && controller.abort()
}
so in above code we will abort controller once only when isMounted is true, and thats will solve your problem
2nd solution (turn around to react changing):
by using useRef hook and asign it to a variable and update its boolean value after excuting the whole code only one time.
const runOnce = useRef(true)
useEffect(()=>{
if(runOnce.current){
//requesting from API
return()=>{
runOnce.current = false
}
}
},[])
3rd solution (turn around to react changing):
remove React.StrictMode from index.js file

Handling axios errors in child components?

My React app has an api client component which handles axios calls to the back end. So, in the api component I have:
async function getData(path: string, params?: any) {
const object: AxiosRequestConfig = {
...obj,
method: 'GET',
headers: {
...obj.headers,
},
params,
};
const response: AxiosResponse = await axios.get(
`${baseUrl}${path}`,
object
);
return response;
});
}
(obj contains the headers and is defined earlier in the file; baseUrl is a constant which I import).
So, if I have a useEffect to retrieve data from the endpoint '/user/{userId}' whenever the state variable userId changes, I do this:
React.useEffect(() => {
const controller = new AbortController();
const getData = async () => {
try {
const url = `user/${userId}`;
let res = await Client.getData(url, {
signal: controller.signal,
});
... Do things with results ...
} catch (e) {
// Show error
if (!controller.signal.aborted) console.log('Error: ', e);
}
};
getData();
return () => {
controller.abort();
};
}, [state.userId]);
I'm just a bit confused about how errors will be handled in this code. So, if there's an error when the axios call is made (eg no network connection, the endpoint is wrong, or the user isn't found or whatever) will the catch block get called in the getData function? Or do I need a try...catch in the api component too?

React Native - I want to set my session state first before I call my API

I am new to React Native.
If someone can help me then would be great.
How I can set my session state first from AsyncStorage before it goes for API call. Because this API call required sessionId (UserId) so it can return only those data which belong to this userId.
The issue I am currently facing is when API calls for the data it is calling with null seesionId instead of some value which I am getting from AsyncStorage because both methods (settingSession, InitList ) are async.
const [sessionId, setSessionId] = useState(null);
const settingSession = async () => {
await AsyncStorage.getItem('userId').then(val => setSessionId(val));
}
useEffect(() => {
settingSession(); // Setting sessionId
InitList(); // Calling API which required session value
}, []);
const InitList = async () => {
var requestOptions = {
method: 'GET',
redirect: 'follow'
};
try {
// getting sessionId null instead of value from AsyncStorage
const response = await fetch("http://127.0.0.1:8080/skyzerguide/referenceGuideFunctions/tetra/user/" + sessionId, requestOptions)
const status = await response.status;
const responseJson = await response.json();
if (status == 204) {
throw new Error('204 - No Content');
} else {
setMasterDataSource(responseJson);
}
} catch (error) {
console.log(error);
return false;
}
}
I'm thinking of two possible solutions:
Separate InitList() into a separate useEffect call, and put sessionId in the dependency array, so that the API call is only made when the sessionId has actually been updated:
useEffect(() => {
settingSession(); // Setting sessionId
}, []);
useEffect(() => {
InitList(); // Calling API which required session value
}, [sessionId]);
Wrap both functions in an async function within the useEffect call, and call them sequentially using await:
useEffect(() => {
const setSessionAndInitList = async() => {
await InitList(); // Calling API which required session value
await settingSession(); // Setting sessionId
}
setSessionAndInitList()
}, []);
Let me know if either works!

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