React - Memory Leak, issues with ComponentWillUnmount / Axios - reactjs

I am performing an API call that kicks off with componentDidMount, however when a new component is loaded by the user, there is a notification for a potential memory leak, see below for message. I have researched different solutions but have found nothing yet that works, I'm wondering if this can be fixed within this particular component with a componentWillUnmount or if it is better handled within the axios call itself.
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.
componentDidMount() {
this.loadBackground();
this.getUpdatedWeather();
this.getNewMartianPhotos();
}
checkMartianPhotos = () => {
if (this.state.martianMotion) {
console.log('still shooting');
this.getNewMartianPhotos();
} else {
return console.log('done now');
}
};
getNewMartianPhotos = () => {
let loadedImage = '';
let loadedInfo = '';
let loadedMeta = '';
let totalImage;
API.getMarsPhotos().then(data => {
// console.log(data.data);
// console.log(
// data.data.collection.items[this.state.martianCount].data[0].photographer
// );
// console.log(
// data.data.collection.items[this.state.martianCount].data[0].description
// );
// console.log(
// data.data.collection.items[this.state.martianCount].links[0].href
// );
totalImage = data.data.collection.items.length;
loadedImage =
data.data.collection.items[this.state.martianCount].links[0].href;
loadedInfo =
data.data.collection.items[this.state.martianCount].data[0].description;
loadedMeta =
data.data.collection.items[this.state.martianCount].data[0]
.photographer;
this.setState({
martianImage: loadedImage,
martianDescription: loadedInfo,
martianMeta: loadedMeta,
martianCount: this.state.martianCount + 1
});
if (this.state.martianCount < totalImage) {
console.log(
`shooting off, image count now ${this.state.martianCount} against ${totalImage}`
);
setTimeout(this.checkMartianPhotos, 10000);
}
});
};
componentWillUnmount() {
clearTimeout(this.checkMartianPhotos);
}
-------------------------------------
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
getMarsPhotos: () =>
axios
.get('https://images-api.nasa.gov/search?q=martian', {
cancelToken: source.token
})
.catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('request canceled', thrown.message);
} else {
console.log('there is an error that needs to be handled');
}
})

As the error informs, your component is calling setState after it has been unMounted. This is because you are clearing your timeout incorrectly. You should be doing the following to properly clear your timeout on unmount.
this.id = setTimeout(this.checkMartianPhotos, 10000);
And then clear with
clearTimeout(this.id)
In componentWillUnmount
Also, try to move your async logic (api calls) out of your component.
Refer to this to see how to stop setting a state after an API call on unmounted component.

Related

Possible leak in React.js axios fetching data and state update

Everything is working with my cde but I have some very strange behavior that I cannot figure out. My returned promises start off with 1 or 1 and slowly get larger until after about 5 minutes it has over dozens of them. I am using this code...
usePolling(function () {
return Promise.resolve(getData())
}, 4000);
The usePolling function...
function usePolling(fetcher, interval) {
const [payload, setPayload] = useState(null);
React.useEffect(function () {
let timeoutId;
function poll() {
timeoutId = setTimeout(function () {
// you must implement the error handling
fetcher()
.then(function (resp) {
setPayload(resp)
poll();
})
}, interval);
}
poll()
return function () {
clearTimeout(timeoutId);
}
}, [fetcher, interval]);
return payload;
}
and the getData fecthing function...
const getData = () => {
const value = Axios.post("http://localhost:3001/api/get-value",
{user: userProp}).then((response) => {
const recievedData = response.data;
const dataValue = recievedData.map((val) => {
return [val.value]
})
if (loading === true){
setLoading(false);
}
setMoisture(parseInt(dataValue))
console.log("sssssssssssssssss")
return parseInt(dataValue);
})
return value
}
my log starts off like this...
and after a few minutes is like this...
What is causing this??
I am not sure why no one could catch this but after completely taking apart my code and rebuilding, I found two main issue causing the render to get out of control. In the getData() function remove this...
if (loading === true){
setLoading(false);
}
setMoisture(parseInt(dataValue))
and instead of using state, use variables like this...
let loading = true;
let moisture;
moisture = usePolling(function () {
return Promise.resolve(getData())
}, 4000);
if (moisture !== null){
loading = false
}
THATS IT!! This solved all of my issues. I am relatively new to react, and therefor state, but this makes sense. React re-renders whenever state changes. However I have asynchronous tasks running so even though my main page has fully rendered, I am still fetching data. Once that promise was fulfilled, the state changed and re-rendered. This caused renders to become very large because my app if rendering every n seconds the way it is set up. Hope this helps someone in the future.

Warning to make a cleanup function in useEffect() occurs occasionally

I am using AWS-Amplify and when a user signs out I get a warning:
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.
It mentions it occurs in the Profile.tsx file and this is the useEffect() hook.
But the issue is that the error occurs sometimes.
I keep testing it out and it comes and goes and I have no idea why it is so.
function Profile() {
const [user, setUser] = useState<IIntialState | null>(null);
useEffect(() => {
checkUser();
Hub.listen("auth", data => {
const { payload } = data;
if (payload.event === "signOut") {
setUser(null);
}
});
}, []);
async function checkUser() {
try {
const data = await Auth.currentUserPoolUser();
const userInfo = { username: data.username, ...data.attributes };
console.log(userInfo);
setUser(userInfo);
} catch (err) {
console.log("error: ", err);
}
}
function signOut() {
Auth.signOut().catch(err => console.log("error signing out: ", err));
}
if (user) {
return (
<Container>
<h1>Profile</h1>
<h2>Username: {user.username}</h2>
<h3>Email: {user.email}</h3>
<h4>Phone: {user.phone_number}</h4>
<Button onClick={signOut}>Sign Out</Button>
</Container>
);
}
return <Form setUser={setUser} />;
}
I happens because you component unmounts but you still have subscriptions in it.
React useEffect provides unmount function:
useEffect(() => {
Hub.listen("auth", func);
return () => {
// unsubscribe here
Hub.remove("auth", signOut)
};
});
And you Hub Class has remove method
remove(channel: string | RegExp, listener: HubCallback): void
Remove your subscription in useEffect return function
Hub.remove()
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.
The message is straightforward. We're trying to change the state of a component, even after it's been unmounted and unavailable.
There are multiple reasons why this can happen but the most common are that we didn’t unsubscribe to a websocket component, or this were dismount before an async operation finished. to solve this you can either do this :
useEffect(() => {
checkUser();
//check howto remove this listner in aws documentation
const listner= Hub.listen("auth", data => {
const { payload } = data;
if (payload.event === "signOut") {
setUser(null);
}
});
//this return function is called on component unmount
return ()=>{/* romve the listner here */}
}, []);
or go with this simple approach .
useEffect(() => {
let mounted =true
Hub.listen("auth", data => {
const { payload } = data;
if (payload.event === "signOut" && mounted ) {
setUser(null);
}
});
//this return function is called on component unmount
return ()=>{mounted =false }
}, []);
Read more about this here.

Call API every 30 seconds in ReactJS [duplicate]

I have to monitoring some data update info on the screen each one or two seconds.
The way I figured that was using this implementation:
componentDidMount() {
this.timer = setInterval(()=> this.getItems(), 1000);
}
componentWillUnmount() {
this.timer = null;
}
getItems() {
fetch(this.getEndpoint('api url endpoint'))
.then(result => result.json())
.then(result => this.setState({ items: result }));
}
Is this the correct approach?
Well, since you have only an API and don't have control over it in order to change it to use sockets, the only way you have is to poll.
As per your polling is concerned, you're doing the decent approach. But there is one catch in your code above.
componentDidMount() {
this.timer = setInterval(()=> this.getItems(), 1000);
}
componentWillUnmount() {
this.timer = null; // here...
}
getItems() {
fetch(this.getEndpoint('api url endpoint'))
.then(result => result.json())
.then(result => this.setState({ items: result }));
}
The issue here is that once your component unmounts, though the reference to interval that you stored in this.timer is set to null, it is not stopped yet. The interval will keep invoking the handler even after your component has been unmounted and will try to setState in a component which no longer exists.
To handle it properly use clearInterval(this.timer) first and then set this.timer = null.
Also, the fetch call is asynchronous, which might cause the same issue. Make it cancelable and cancel if any fetch is incomplete.
I hope this helps.
Although an old question it was the top result when I searched for React Polling and didn't have an answer that worked with Hooks.
// utils.js
import React, { useState, useEffect, useRef } from 'react';
export const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
You can then just import and use.
// MyPage.js
import useInterval from '../utils';
const MyPage = () => {
useInterval(() => {
// put your interval code here.
}, 1000 * 10);
return <div>my page content</div>;
}
You could use a combination of setTimeout and clearTimeout.
setInterval would fire the API call every 'x' seconds irrespective whether the previous call succeeded or failed. This can eat into your browser memory and degrade performance over time. Moreover, if the server is down, setInterval would continue to bombard the server not knowing its down status.
Whereas,
You could do a recursion using setTimeout. Fire a subsequent API call, only if the previous API call succeed. If previous call has failed, clear the timeout and do not fire any further calls. if required, alert the user on failure. Let the user refresh the page to restart this process.
Here is an example code:
let apiTimeout = setTimeout(fetchAPIData, 1000);
function fetchAPIData(){
fetch('API_END_POINT')
.then(res => {
if(res.statusCode == 200){
// Process the response and update the view.
// Recreate a setTimeout API call which will be fired after 1 second.
apiTimeout = setTimeout(fetchAPIData, 1000);
}else{
clearTimeout(apiTimeout);
// Failure case. If required, alert the user.
}
})
.fail(function(){
clearTimeout(apiTimeout);
// Failure case. If required, alert the user.
});
}
#AmitJS94, there's a detailed section on how to stop an interval that adds onto the methods that GavKilbride mentioned in this article.
The author says to add a state for a delay variable, and to pass in "null" for that delay when you want to pause the interval:
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
Definitely read the article to get a better understanding of the details -- it's super thorough and well-written!
As Vasanth mention, I preferred to:
use setTimeout to measure the time between the end of the last request and the beginning of the next one
make the first request straight away, not after the delay
inspired by the answer from #KyleMit https://stackoverflow.com/a/64654157/343900
import { useEffect, useRef } from 'react';
export const useInterval = (
callback: Function,
fnCondition: Function,
delay: number,
) => {
const savedCallback = useRef<Function>();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
let id: NodeJS.Timeout;
const tick = async () => {
try {
const response =
typeof savedCallback.current === 'function' &&
(await savedCallback.current());
if (fnCondition(response)) {
id = setTimeout(tick, delay);
} else {
clearTimeout(id);
}
} catch (e) {
console.error(e);
}
};
tick();
return () => id && clearTimeout(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay]);
};
WORKS: Using fnCondition inside which can be a condition based on the response from the last request.
//axios-hooks
const {
data,
isLoadingData,
getData,
} = api.useGetData();
const fnCondition = (result: any) => {
const randomContidion = Math.random();
//return true to continue
return randomContidion < 0.9;
};
useInterval(() => getData(), fnCondition, 1000);
DOES NOT WORK: Passing delay as null to stop useInterval like this does not work for me
with this code: https://www.aaron-powell.com/posts/2019-09-23-recursive-settimeout-with-react-hooks/
(You might get the impression it works, but after a few starts/stops it breaks)
const [isRunning, setIsRunning] = useState(true);
const handleOnclick = () => {
setIsRunning(!isRunning);
};
useInterval(() => getData(), isRunning ? 1000 : null);
<button onClick={handleOnclick}>{isRunning ? 'Stop' : 'Start'}</button>
Sum up: I'm able to stop useInterval by passing fnCondition, but not by passing delay=null
Here's a simple, full solution, that:
Polls every X seconds
Has the option of increasing the timeout each time the logic runs so you don't overload the server
Clears the timeouts when the end user exits the component
//mount data
componentDidMount() {
//run this function to get your data for the first time
this.getYourData();
//use the setTimeout to poll continuously, but each time increase the timer
this.timer = setTimeout(this.timeoutIncreaser, this.timeoutCounter);
}
//unmounting process
componentWillUnmount() {
this.timer = null; //clear variable
this.timeoutIncreaser = null; //clear function that resets timer
}
//increase by timeout by certain amount each time this is ran, and call fetchData() to reload screen
timeoutIncreaser = () => {
this.timeoutCounter += 1000 * 2; //increase timeout by 2 seconds every time
this.getYourData(); //this can be any function that you want ran every x seconds
setTimeout(this.timeoutIncreaser, this.timeoutCounter);
}
Here is a simple example using hooks in function component and this will refresh your data in a set interval.
import React from 'react';
import { useEffect, useState } from 'react';
export default function App() {
let [jokes, setJokes] = useState('Initial');
async function fetchJokes() {
let a = await fetch('https://api.chucknorris.io/jokes/random');
let b = await a.json();
setJokes(b.value);
}
// Below function works like compomentWillUnmount and hence it clears the timeout
useEffect(() => {
let id = setTimeout(fetchJokes, 2000);
return () => clearTimeout(id);
});
return <div>{jokes}</div>;
}
or, you can use axios as well to make the API calls.
function App() {
const [state, setState] = useState("Loading.....");
function fetchData() {
axios.get(`https://api.chucknorris.io/jokes/random`).then((response) => {
setState(response.data.value);
});
}
useEffect(() => {
console.log("Hi there!");
let timerId = setTimeout(fetchData, 2000);
return ()=> clearInterval(timerId);
});
return (
<>
This component
<h3>{state}</h3>
</>
);
}

What is the right way to cancel all async/await tasks within an useEffect hook to prevent memory leaks in react?

I am working on a react chap app that pulls data from a firebase database. In my "Dashboard" component I have an useEffect hook checking for an authenticated user and if so, pull data from firebase and set the state of a an email variable and chats variable. I use abortController for my useEffect cleanup, however whenever I first log out and log back in I get a memory leak warning.
index.js:1375 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.
in Dashboard (created by Context.Consumer)
Originally I didn't have the abortController, I just returned a console log on clean up. Did more research and found abortController however the examples use fetch and signal and I could not find any resources on using with async/await. I am open to changing how the data is retrieved, (whether that is with fetch, async/await, or any other solution) I just have not been able to get it working with the other methods.
const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);
const signOut = () => {
firebase.auth().signOut();
};
useEffect(() => {
const abortController = new AbortController();
firebase.auth().onAuthStateChanged(async _user => {
if (!_user) {
history.push('/login');
} else {
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.onSnapshot(async res => {
const chatsMap = res.docs.map(_doc => _doc.data());
console.log('res:', res.docs);
await setEmail(_user.email);
await setChats(chatsMap);
});
}
});
return () => {
abortController.abort();
console.log('aborting...');
};
}, [history, setEmail, setChats]);
Expected result is to properly cleanup/cancel all asynchronous tasks in a useEffect cleanup function. After one user logs out then either the same or different user log back in I get the following warning in the console
index.js:1375 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.
in Dashboard (created by Context.Consumer)
In the case of firebase you aren't dealing with async/await but streams. You should just unsubscribe from firebase streams in cleanup function:
const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);
const signOut = () => {
firebase.auth().signOut();
};
useEffect(() => {
let unsubscribeSnapshot;
const unsubscribeAuth = firebase.auth().onAuthStateChanged(_user => {
// you're not dealing with promises but streams so async/await is not needed here
if (!_user) {
history.push('/login');
} else {
unsubscribeSnapshot = firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.onSnapshot(res => {
const chatsMap = res.docs.map(_doc => _doc.data());
console.log('res:', res.docs);
setEmail(_user.email);
setChats(chatsMap);
});
}
});
return () => {
unsubscribeAuth();
unsubscribeSnapshot && unsubscribeSnapshot();
};
}, [history]); // setters are stable between renders so you don't have to put them here
The onSnapshot method does not return a promise, so there's no sense in awaiting its result. Instead it starts listening for the data (and changes to that data), and calls the onSnapshot callback with the relevant data. This can happen multiple times, hence it can't return a promise. The listener stays attached to the database until you unsubscribe it by calling the method that is returned from onSnapshot. Since you never store that method, let alone call it, the listener stays active, and will later again call your callback. This is likely where the memory leak comes from.
If you want to wait for the result from Firestore, you're probably looking for the get() method. This gets the data once, and then resolves the promise.
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.get(async res => {
One way to cancel async/await is to create something like built-in AbortController that will return two functions: one for cancelling and one for checking for cancelation, and then before each step in async/await a check for cancellation needs to be run:
function $AbortController() {
let res, rej;
const p = new Promise((resolve, reject) => {
res = resolve;
rej = () => reject($AbortController.cSymbol);
})
function isCanceled() {
return Promise.race([p, Promise.resolve()]);
}
return [
rej,
isCanceled
];
}
$AbortController.cSymbol = Symbol("cancel");
function delay(t) {
return new Promise((res) => {
setTimeout(res, t);
})
}
let cancel, isCanceled;
document.getElementById("start-logging").addEventListener("click", async (e) => {
try {
cancel && cancel();
[cancel, isCanceled] = $AbortController();
const lisCanceled = isCanceled;
while(true) {
await lisCanceled(); // check for cancellation
document.getElementById("container").insertAdjacentHTML("beforeend", `<p>${Date.now()}</p>`);
await delay(2000);
}
} catch (e) {
if(e === $AbortController.cSymbol) {
console.log("cancelled");
}
}
})
document.getElementById("cancel-logging").addEventListener("click", () => cancel())
<button id="start-logging">start logging</button>
<button id="cancel-logging">cancel logging</button>
<div id="container"></div>

ReactJS: Error while solving memory leak using makeCancellable method

I have encountered a memory leak error on in my react application. The error occurs when API call is made. My application renders 3 times because header and footer got setState and then todoList using setState.
Console error below
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. index.js:1446
I have tried _.isMounted method to solve the issue and worked also but then the solution is deprecated.
isMounted method code below ...
_isMounted = false
componentDidMount() {
this._isMounted = true
API.getTodoList().then(data => {
if (this._isMounted) {
this.setState({ itemList: data.data.itemList });
}
})
}
componentWillUnmount() {
this._isMounted = false
}
Later I tried makeCancelable method to fix memory leak. But it didnt solve the issue and got same memory leak error and another error from .catch()
API call:
// makeCancelable fn is defined at start
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => hasCanceled_ ? reject({ isCanceled: true }) : resolve(val),
error => hasCanceled_ ? reject({ isCanceled: true }) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
componentDidMount() {
console.log("didMount")
this.cancelRequest = makeCancelable(
axiosClient.get('/todoList')
.then((response) => {
this.setState({ itemList: response.data.data.itemList })
})
.catch(({ isCanceled, ...error }) => console.log('isCanceled', isCanceled))
)
}
componentWillUnmount() {
console.log("componentUnmount")
this.cancelRequest.cancel();
}
Is there any other way to solve memory leak error without using _.isMounted method.
I would appreciate the help.
The message warns against the possibility of memory leak. It doesn't states that there is one, although original code can result in memory leak, depending on how the request is performed
makeCancelable is misused, it cannot cause the entire promise chain it wraps to not be executed because promises aren't cancellable.
It should be:
this.cancelRequest = makeCancelable(
axiosClient.get('/todoList')
);
cancelRequest.promise
.then(...)
.catch(({ isCanceled, ...error }) => console.log('isCanceled', isCanceled))
There's no need to do this because Axios already provides cancellation:
this.cancelRequest = axios.CancelToken.source();
axiosClient.get('/todoList', { cancel: this.cancelRequest.token })
.then(...)
.catch(error => console.log('isCanceled', axios.isCancel(error)))

Resources