ReactJS memory leak while async tasks & subscriptions - reactjs

In my react app I am making API call using axios. But, in my console, I got an 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."
My code is below
To fix the memory leak, I added _isMounted check and consoled the _ismounted. My app is rendered twice.
First-time console prints _isMounted status true and then false (due to componentDidMount) but then the app is rendered second time & _isMounted in the console is printed as true.
app.jsx
export class Test extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
apiData: null
}
}
componentDidMount() {
this._isMounted = true;
API.callAPI(this.setStateHandler, this._isMounted)
}
setStateHandler = (state) => {
this.setState(state);
}
componentWillUnmount() {
this._isMounted = false
}
render() {
return(...)}
api.js
callAPI = (setStateHandler, _isMounted) => {
axios.get('/article', {headers: {Authorization: token}})
.then((response) => {
if(_isMounted) {
setStateHandler({ programs: response.data.data.programs });
}
})
}
I would like to fix my memory leak issue. How should I fix that?
Why is my application rendered twice and the second time componentDidUnmount is not called?
I would appreciate the help.

The _isMounted argument passed in to callAPI will not change when your component is unmounted.
You could instead return the data to the component and check if this._isMounted is still true there instead.
Example
// App.jsx
export class Test extends Component {
_isMounted = false;
state = { programs: null };
componentDidMount() {
this._isMounted = true;
API.callAPI().then(data => {
if (this._isMounted) {
this.setState({ programs: data.data.programs });
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
// ...
}
}
// api.js
const callAPI = () => {
return axios
.get("/article", { headers: { Authorization: token } })
.then(response => response.data);
};

Related

A lot of unnecessary requests

I have Posts component:
class ProfilePosts extends React.Component {
constructor(props){
super(props);
this.state = {
posts: []
}
}
getData = async (url) => {
const res = await fetch(url, {
method: 'GET',
headers: {
'Authorization' : `${window.localStorage.getItem('token')}`
}
});
return await res.json();
}
getPosts = async () => {
if (window.location.pathname === '/profile/undefined%7D')
{
window.location.pathname = `/profile/${window.localStorage.getItem('id')}`
}
await this.getData(`https://api.com/posts/${window.location.pathname.slice(9)}`)
.then(data => {
this.setState({posts: data}
);
})
}
componentDidMount(){
if (window.localStorage.getItem('id') != ''){
this.getPosts();
}
}
componentDidUpdate(){
if (window.localStorage.getItem('id') != ''){
this.getPosts();
}
}
renderItems(posts){
return Object.values(posts).map(post => {
return (
<ProfilePost likes={post.likes} comments={post.comments} userId={window.localStorage.getItem('id')} token={window.localStorage.getItem('token')} Postid={post.id} key={post.id} sender={post.sender} content={post.content} time={post.sent_time}/>
)
});
}
render() {
const {posts} = this.state;
const items = this.renderItems(posts);
return(
<div className="profile-posts">
{items}
</div>
);}
}
export default ProfilePosts;
I don't understand what is the reasons for re-rendering this component so many times (I don't know the actual number but it's huge). Therefore, a lot of requests to the database, and so on. How can I fix this?
I tried using another state but that didn't work for me. If you press the button, the state changed and then I checked if the state changed then I call this.getPosts() and then set the state to false again. It caused Error: Maximum update depth exceeded.
Your componentDidUpdate runs every time the component updates, after render, so it's calling this.getPosts();, which in turn calls setState, which results in another re-render and update, and so on.
Remove the componentDidUpdate entirely and let the componentDidMount method (which implements the same logic) alone take care of things.

Unable to setState regardless of making setState synchronous

I am learning about how to use synchronous setState but it is not working for my project. I want to update the state after I get the listingInfo from Axios but it does not work, the res.data, however, is working fine
class ListingItem extends Component {
constructor(props) {
super(props);
this.state = {
listingInfo: {},
open: false,
};
this.getListingData(this.props.itemId);
}
setStateSynchronous(stateUpdate) {
return new Promise((resolve) => {
this.setState(stateUpdate, () => resolve());
});
}
getListingData = async (item_id) => {
try {
const res = await axios.get(`http://localhost:5000/api/items/${item_id}`);
console.log(res.data);//it's working
await this.setStateSynchronous({ listingInfo: res.data });
// this.setState({
// listingInfo: res.data,
// });
console.log(this.state.listingInfo);//no result
} catch (err) {
setAlert('Fail to obtain listings', 'error');
}
};
I would be really grateful for your help!
Thanks to #PrathapReddy! I used conditional rendering to prevent the data from rendering before the setState is done. I added this line of code on the rendering part:
render() {
if (Object.keys(this.state.listingInfo).length === 0) {
return (
<div>
Loading
</div>
);
} else {
return //put what you want to initially render here
}
}
Also, there is no need to modify the setState, the normal setState will do. Hope this is useful!

React componentDidMount and working with Promises?

Getting really fed up now! I am trying to get a Spinner element to appear while 3 functions run in the componentDidMount function.
From what I gather the render comes before componentDidMount, so I am running the Spinner in the render, while:
a cookie value is retrieved from this.getValidToken()
then an axios post request sets state of isLoggedin (using above value as payload)
then the logic() function runs a simple if statement to either log user in or redirect to
error page.
I keep getting errors about Promises, I feel there is a better way to do this?
constructor(props){
super(props);
this.state = {
isLoggedIn: false
}
}
componentDidMount() {
const post =
axios.post(//api post request here)
.then(function(response) {
this.setState({ isLoggedIn: true });
})
.catch(function(error) {
this.setState({ isLoggedIn: false });
})
const LoggedIn = this.state.isLoggedIn;
const logic = () => {
if (LoggedIn) {
//log user in
} else {
//redirect user to another page
}
};
this.getValidToken()
.then(post)
.then(logic);
//getValidToken firstly gets a cookie value which is then a payload for the post function
}
render() {
return <Spinner />;
}
Firstly, you assign axios post to a variable, it is executed immediately and not after the getValidToken promise is resoved
Secondly the state update in react is async so you cannot have loggedIn logic based on state in promise resolver
You could handle the above scenario something like
constructor(props){
super(props);
this.state = {
isLoggedIn: false
}
}
componentDidMount() {
const post = () => axios.post(//api post request here)
.then(function(response) {
this.setState({ isLoggedIn: true });
return true;
})
.catch(function(error) {
this.setState({ isLoggedIn: false });
return false;
})
const logic = (isLoggedIn) => { // use promise chaining here
if (isLoggedIn) {
//log user in
} else {
//redirect user to another page
}
};
this.getValidToken()
.then(post)
.then(logic);
//getValidToken firstly gets a cookie value which is then a payload for the post function
}
render() {
return <Spinner />;
}

React accessing state before ComponentDidMount

When I try to access a state variable which is set in ComponentDidMount, react throws an undefined error. This is because I believe when I'm calling the fetch api and setState in ComponentDidMount, the value isn't ready yet (async stuff). Is there a proper way to either delay the render until the setState call is done or some other way to get the state updated fully before render is called?
I think the code below will give you a basic idea how fetch data and render work.
class App extends Component {
state = {
data:{},
loading:true,
error:null,
}
componentDidMount = () => {
fetch('https://example.com/api/article')
.then((response) => {
return response.json();
})
.then((json) => {
this.setState({
data:json,
loading:false,
})
.catch(error => {
this.setState({
error,
loading:false,
})
});
});
}
render() {
const {data,error,loading} = this.state;
if(loading){
return "Loading ..."
}
if(error){
return "Something went wrong."
}
return 'your actual render component or data';
}
}
export default App;

componentDidMount: Can't call setState (or forceUpdate) on an unmounted component

I am fetching data in componentDidMount and updating the state and the famous warning is appearing:
Warning: Can't call setState (or forceUpdate) 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.
My code is as follow:
componentDidMount() {
let self = this;
let apiBaseUrl = Config.serverUrl;
axios.get( apiBaseUrl + '/dataToBeFetched/' )
.then( function(response) {
self.setState( { data: response.data } );;
} );
}
What is causing this warning and what is the best way to fetch the data and update the state?
Based on a previous answer, I have done the following which worked fine:
constructor(props) {
this.state = {isMounted: false}
}
componentDidMount() {
let apiBaseUrl = Config.serverUrl;
this.setState( { isMounted: true }, () => {
axios.get( apiBaseUrl + '/dataToBeFetched/' )
.then( (response) => { // using arrow function ES6
if( this.state.isMounted ) {
this.setState( { pets: response.data } );
}
} ).catch( error => {
// handle error
} )
} );
}
componentWillUnmount() {
this.setState( { isMounted: false } )
}
Another better solution is to cancel the request in the unmount as follows:
constructor(props) {
this._source = axios.CancelToken.source();
}
componentDidMount() {
let apiBaseUrl = Config.serverUrl;
axios.get( apiBaseUrl + '/dataToBeFetched/', { cancelToken: this._source.token } )
.then( (response) => { // using arrow function ES6
if( this.state.isMounted ) {
this.setState( { pets: response.data } );
}
} ).catch( error => {
// handle error
} );
}
componentWillUnmount() {
this._source.cancel( 'Operation canceled due component being unmounted.' )
}
You can try this:
constructor() {
super();
this._isMounted = false;
}
componentDidMount() {
this._isMounted = true;
let apiBaseUrl = Config.serverUrl;
this.setState( { isMounted: true }, () => {
axios.get( apiBaseUrl + '/dataToBeFetched/' )
.then( (response) => { // using arrow function ES6
if( this._isMounted ) {
this.setState( { pets: response.data } );
}
} ).catch( error => {
// handle error
} )
} );
}
componentWillUnmount() {
this._isMounted = false; // equals, not :
}
This most likely happened because the component was already unmounted before the async call finished. Meaning, your call to setState in the axios promise is being called after the component is already unmounted, perhaps because of a react-router Redirect or a change in state?
Call setState on componentWillUnmount is the worst practice
componentWillUnmount() {
this.setState( { isMounted: false } ) // don't do this
}

Resources