useEffect cleanup function in react - reactjs

I get this 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 a useEffect cleanup function.'
Code
const [photo, setPhoto] = useState([]);
useEffect(() => {
fetch('/mypost', {
headers: {
cookie: 'access_token',
},
})
.then((res) => res.json())
.then((data) => {
setPhoto(data.mypost);
});
}, []);
The data is fetched but I could'nt figure what to add in the clean up. Any suggestions?

Issue
The problem is that the fetch request resolves but the component has unmounted (for some reason) and so the state update can't occur now.
Solution
You need to use an Abort Controller to cancel in-flight requests. If the component unmounts, an effect cleanup function cancels the fetch request.
useEffect(() => {
const controller = new AbortController(); // <-- create controller
const { signal } = controller; // <-- get signal for request
fetch('/mypost', {
headers: {
cookie: 'access_token',
},
signal, // <-- pass signal with options
})
.then((res) => res.json())
.then((data) => {
setPhoto(data.mypost);
});
return () => controller.abort(); // <-- return cleanup function to abort
}, []);
Note: When abort() is called, the fetch() promise rejects with an
AbortError.
You will likely need to catch this promise rejection somewhere. You can append a .catch block to the Promise chain.
fetch('/mypost', {
headers: {
cookie: 'access_token',
},
signal, // <-- pass signal with options
})
.then((res) => res.json())
.then((data) => {
setPhoto(data.mypost);
})
// catch any rejected fetch promises (including aborted fetches!)
.catch(console.error);

Generic JSON fetch demo
import React, {useState} from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpFetch from "cp-fetch";
export default function TestComponent(props) {
const [photo, setPhoto] = useState([]);
useAsyncEffect(
function* () {
const response = yield cpFetch('/mypost');
setPhoto((yield response.json()).mypost);
},
[]
);
return <div></div>;
}

try the following code:
useEffect(() => {
let isMounted = true;
fetch('/mypost', {
headers: {
cookie: 'access_token',
},
})
.then((res) => res.json())
.then((data) => {
if (isMounted) setPhoto(data.mypost);
});
//cleanup function
return () => { isMounted = false };
}, []);

Related

Fetching an array of objects from POKEAPI using REACT.js and AXIOS {Answered}

I chose to start learning API handling with POKEAPI. I am at a step where I need to get the flavor_text of each pokemon (the description let's say) but I can't for some reason.
Here is the JSON structure for one specific pokemon: https://pokeapi.co/api/v2/pokemon-species/bulbasaur.
And here is my useEffect trying to get it. The line fetching the habitat works and displays on my website so I guess my issue comes from my map in setDescription but I can't be sure.
export default function Card({ pokemon }, { key }) {
const src = url + `${pokemon.id}` + ".png";
const [habitat, setHabitat] = useState(null);
const [descriptions, setDescriptions] = useState([]);
useEffect(() => {
const controller = new AbortController();
axios
.get(url2 + `${pokemon.name}`, { signal: controller.signal })
.then((res) => setHabitat(res.data.habitat.name))
.then((res) =>
setDescriptions(
res.data.flavor_text_entries.map((ob) => ob.flavor_text)
)
)
.catch((err) => {
if (axios.isCancel(err)) {
} else {
console.log("warning your useEffect is behaving");
}
});
return () => {
// cancel the request before component unmounts
controller.abort();
};
}, [pokemon]);
I tried console logging descriptions or descriptions[0] but that doesn't work.
Since you only setting up the state from those data and it doesn't looks like the second result need to wait the result from the first to perform you can do both on the same response/promise :
useEffect(() => {
const controller = new AbortController();
axios
.get(url2 + `${pokemon.name}`, { signal: controller.signal })
.then((res) => {
setHabitat(res.data.habitat.name))
const flavorTextEntrieList = res.data.flavor_text_entries;
setDescriptions(flavorTextEntrieList.map((ob) => ob.flavor_text))
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
console.log("warning your useEffect is behaving");
}
});
return () => {
// cancel the request before component unmounts
controller.abort();
};
}, [pokemon]);
Each then need to return something to be handled in next chainable then. Replace .then((res) => setHabitat(res.data.habitat.name)) with .then((res) => { setHabitat(res.data.habitat.name); return res; })

How to integrate AbortController with Axios and React?

The Abortcontroller signal is not working for me with Axios in React.
I wanted to replace CancelToken (as it's deprecated) with the AbortController, but it is not working, respectively the requests are not being canceled.
let testController: AbortController;
function loadTest() {
testController = new AbortController();
TestAPI.getTest(testController.signal)
.then((e) => {
console.log(e.data);
})
.catch((e) => {
console.error(e);
});
}
Also in the UseEffect Cleanup I do this (here it should cancel) and also the signal's state is set to aborted, but still the request is not canceled:
useEffect(() => () => {
if (testController) testController.abort();
// console.log(testController.signal.aborted) => **true**
}, []);
Here is my API, where I pass the AbortSignal to the request:
getTest(signal?: AbortSignal): Promise<AxiosResponse<Test[]>> {
return axios.get(`${URI}/test`, { signal });
},
When using Axios.CancelToken.source was working fine, but now with the AbortController, the request is never canceled.
Using: "axios": "^0.26.0",
Did someone manage to integrate the AbortController with React and Axios? Or does the AbortController only work with fetch?
The axios.CancelToken API isn't deprecated as far as I can tell, it's still in the spec, but according to the docs axios also supports the AbortController of the fetch API.
Cancellation
Axios supports AbortController to abort requests in fetch API way:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
It's not clear exactly where testController is declared:
let testController: AbortController;
but I suspect it's in the body of the function component and redeclared on a subsequent render cycle.
I suggest using a React ref to store an AbortController, and reference this ref value around your app. This is so the component holds on to a stable reference of the controller from render cycle to render cycle, to be referenced in any useEffect hook cleanup function to cancel in-flight requests if/when the component unmounts.
const abortControllerRef = useRef<AbortController>(new AbortController());
function loadTest() {
TestAPI.getTest(abortControllerRef.current.signal)
.then((e) => {
console.log(e.data);
})
.catch((e) => {
console.error(e);
});
}
useEffect(() => {
const controller = abortControllerRef.current;
return () => {
controller.abort();
};
}, []);
I would recommend to read this post.
In a nutshell you would like to use useEffect to create controller, and, what is more important, to use return statement to abort the controller.
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
getData(signal)
//cleanup function
return () => {controller.abort();};
}, [fetchClick]);
getData function can then be your axious call in the form:
const getData = async (signal) =>{
const res = await axios.get(url, {signal: signal}).then(...)
}
Abort controller often use in useEffect to fetch some data. So, in order to implement the control you can try this:
//...
const [data, setData] = useState([]);
useEffect(() => {
const controller = new AbortController();
axios
.get("https://somedata.com", { signal: controller.signal })
.then(res => {
setData(res.data);
})
.catch(err => console.log(err));
// return cleanup function to abort request
return () => {
controller.abort();
};
}, []);
//...
There's my code example, hope this helps:
useEffect(() => {
const abortController = new AbortController();
const getData = async () => {
try {
const res = await axios("/api/data/", {
signal: abortController.signal,
});
const data = res.data
} catch (error) {
if (error.name !== "CanceledError") {
/* Logic for non-aborted error handling goes here. */
console.log('error:', error)
}
}
};
getData();
// clean up function when unmounted to avoid getData fired twice problem in React 18
return () => abortController.abort();
}, []);
All you need regarding AbortController with axios here
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()

Warning: Can't perform a React state update on an unmounted component in React native

I am getting data from axios.But sometime data is coming and some it is not showing data initially.
get-specific-salary-api.js:---
const AllSpecificSalaryAPI = (yearMonthFilter) => {
const [specificAllAPIData, setAllSpecificAPIData] = useState("");
const loginAuthToken = useSelector(
(state) => state.loginAuthTokenReducer.loginAuthToken
);
//NOTE:taking oved input and company selection code for dynamic parameter
const companyValue = useSelector(
(state) => state.changeCompanyReducer.company
);
const companyCode = companyValue[0];
const employeeId = companyValue[1];
useEffect(() => {
axios
.get(GET_ALL_SPECIFIC_SALARY, {
params: {
hev: companyCode,
year_month: yearMonthFilter,
oved: employeeId,
},
headers: {
Authorization: `Bearer ${loginAuthToken}`,
},
})
.then((response) => response.data)
.then((data) => setAllSpecificAPIData(data))
.catch((error) => {
if (error.status === 401) {
//NOTE: handling token expire
return ExpireAlertRestart();
} else {
Alert.alert(error.message);
}
});
}, []);
return {
specificAllAPI: specificAllAPIData,
};
};
export default AllSpecificSalaryAPI;
i am getting warning message for this.
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 a useEffect cleanup function.
at [native code]:null in dispatchAction
at src/api/get-specific-salary-api.js:32:12 in axios.get.then.then$argument_0
How can i solve this warning message?
In your useEffect, clean your states like so:
const [specificAllAPIData, setAllSpecificAPIData] = useState(null);
useEffect(() => {
return () => {
setAllSpecificAPIData(null);
};
}, []);
Please see a better explanation on how useEffect is working here
You are getting this because the component tries to update the state even when the component has unmounted.
To solve this, you can use an indicator variable that tells about the unmounting of the component. On unmounting, it will intercept the request of state update in between and cancel that.
let hasUnmounted = false;
useEffect(() => {
axios
.get(GET_ALL_SPECIFIC_SALARY, {
params: {
hev: companyCode,
year_month: yearMonthFilter,
oved: employeeId,
},
headers: {
Authorization: `Bearer ${loginAuthToken}`,
},
})
.then((response) => response.data)
.then((data) => {
if (hasUnmounted) return;
setAllSpecificAPIData(data)
})
.catch((error) => {
if (hasUnmounted) return;
if (error.status === 401) {
//NOTE: handling token expire
return ExpireAlertRestart();
} else {
Alert.alert(error.message);
}
});
return () => {
hasUnmounted = true;
}
}, []);
Check this link for implementation: https://codesandbox.io/s/happy-swartz-ikqdn?file=/src/updateErr.js
Note: go to https://ikqdn.csb.app/err in codesandbox's browser

Call API only after setting loading state

Since, setState in a functional component do not return a promise, how do we set a loading state and then call an API. I have seen people doing it like the one below. I think the axios call will not wait for the loading state to be successfully set before executing. Is there any other better way to solve this without writing the fetch part in an useEffect with the dependency of the loading state?
useEffect(() => {
const fetchProduct = async () => {
setLoading(true);
try {
const response = await axios('http://localhost/products');
setData(response.data);
} catch (err) {
setError(err);
}
setLoading(false);
};
fetchProduct();
}, [productId]);
you can try something like this
useEffect(() => {
const fetchProduct = async () => {
setLoading(true);
await axios.get('http://localhost/products')
.then(response => {
setLoading(false);
setData(response.data);
}).catch(error => {
setLoading(false);
setError(error);
})
};
fetchProduct();
}, [productId]);

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