Calling setState on an unmounted component - javascript

In a lot of my components I need to do something like this:
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
.then(() => {
return this.props.onSuccess()
})
.finally(() => this.setState({loading: false}))
}
The onSuccess function
may or may not be a promise (if it is, loading should stay true until it is resolved)
may or may not unmount the component (it may close the modal this component is in or even navigate to different page)
If the function unmounts the component, this.setState({loading: false}) obviously triggers a warning Can't call setState (or forceUpdate) on an unmounted component.
My 2 questions:
Is there a simple way to avoid the issue ? I don't want to set some _isMounted variable in componentDidMount and componentWillUnmount and then check it when needed in most of my components, plus I may forget to do it next time writing something like this ...
Is it really a problem ? I know that, according to the warning, it indicates a memory leak in my application, but it is not a memory leak in this case, is it ? Maybe ignoring the warning would be ok ...
EDIT: The second question is a little bit more important for me than the first. If this really is a problem and I just can't call setState on unmounted component, I'd probably find some workaround myself. But I am curious if I can't just ignore it.
Live example of the problem:
const someAsyncFunc = () => new Promise(resolve => {
setTimeout(() => {
console.log("someAsyncFunc resolving");
resolve("done");
}, 2000);
});
class Example extends React.Component {
constructor(...args) {
super(...args);
this.state = {loading: false};
}
componentDidMount() {
setTimeout(() => this.handleSubmit(), 100);
}
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
/*
.then(() => {
return this.props.onSuccess()
})
*/
.finally(() => this.setState({loading: false}))
}
render() {
return <div>{String(this.state.loading)}</div>;
}
}
class Wrapper extends React.Component {
constructor(props, ...rest) {
super(props, ...rest);
this.state = {
children: props.children
};
}
componentDidMount() {
setTimeout(() => {
console.log("removing");
this.setState({children: []});
}, 1500)
}
render() {
return <div>{this.state.children}</div>;
}
}
ReactDOM.render(
<Wrapper>
<Example />
</Wrapper>,
document.getElementById("root")
);
.as-console-wrapper {
max-height: 100% !important;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>

Unfortunately you have to keep track of "isMounted" yourself.
To simplify you control flow you could use async/await:
handleSubmit() {
this.setState({loading: true})
try {
await someAsyncFunction()
await this.props.onSuccess()
} finally {
if (this._isMounted) {
this.setState({loading: false})
}
}
}
This is actually mentioned in the react docs, which points to this solution: https://gist.github.com/bvaughn/982ab689a41097237f6e9860db7ca8d6
If your someAsyncFunction supports cancelation, you should do so in componentWillUnmount, as encouraged by this article. But then - of course - check the return value and eventually not call this.props.onSuccess.

class myClass extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
this._isMounted = true;
this._getData();
}
componentWillUnmount() {
this._isMounted = false;
}
_getData() {
axios.get('example.com').then(data => {
if (this._isMounted) {
this.setState({ data })
}
});
}
render() {
...
}
}

You should be able to use this._isMounted to check if the component is actually mounted.
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
.then(() => {
return this.props.onSuccess()
})
.finally(() => {
if (this && this._isMounted) { // check if component is still mounted
this.setState({loading: false})
}
})
}
But be aware that this approach is considered to be an anitpattern. https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html

What about
componentWillUnmount() {
// Assign this.setState to empty function to avoid console warning
// when this.setState is called on an unmounted component
this.setState = () => undefined;
}

Related

Can I fetch api every time that props change on ComponentDidUpdate?

As you see my code, I want to fetch API contentUrl every its change from props.
but browser throw error like this. Have someone help me?.
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.
Issue is clearly states that : Can't perform a React state update on
an unmounted component
So check if the component is unmounted before setting up state, _isMounted
Code Snippet Ref from : HERE , Hope this will clear all your doubts
class News extends Component {
_isMounted = false; // <----- HERE
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true; // <----- HERE
axios
.get('YOUR_URL')
.then(result => {
if (this._isMounted) { // <----- CHECK HERE BEFORE SETTING UP STATE
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false; // <----- HERE
}
render() {
...
}
}
For example, if a call is done and your component is unmounted, a setState will be called.
You can prevent this with a condition:
_isMount = true;
componentDidUpdate() {
this.props.getContentJSON(url).then(() => {
if(this._isMount){
this.setState({...})
}
})
}
componentWillUnmount() {
this._isMount = false;
}
or control your call :
controller = new AbortController();
componentDidUpdate() {
// I use fetch here but you can adapte to your code
fetch(url, { signal: controller.signal }).then(() => {
this.setState({...})
})
}
componentWillUnmount() {
controller.abort();
}

Updating the react state/component correctly?

I am trying to make my component reactive on updates. I am using componentDidUpdate() to check if the components prop state has changed, then if it has it is has I need the getPosts() function to be called and the postCount to update if that prop is changed.
export default class JsonFeed extends React.Component<IJsonFeedProps, IJsonFeedState> {
// Props & state needed for the component
constructor(props) {
super(props);
this.state = {
description: this.props.description,
posts: [],
isLoading: true,
jsonUrl: this.props.jsonUrl,
postCount: this.props.postCount,
errors: null,
error: null
};
}
// This function runs when a prop choice has been updated
componentDidUpdate() {
// Typical usage (don't forget to compare props):
if (this.state !== this.state) {
this.getPosts();
// something else ????
}
}
// This function runs when component is first renderd
public componentDidMount() {
this.getPosts();
}
// Grabs the posts from the json url
public getPosts() {
axios
.get("https://cors-anywhere.herokuapp.com/" + this.props.jsonUrl)
.then(response =>
response.data.map(post => ({
id: `${post.Id}`,
name: `${post.Name}`,
summary: `${post.Summary}`,
url: `${post.AbsoluteUrl}`
}))
)
.then(posts => {
this.setState({
posts,
isLoading: false
});
})
// We can still use the `.catch()` method since axios is promise-based
.catch(error => this.setState({ error, isLoading: false }));
}
You can change componentDidUpdate to:
componentDidUpdate() {
if (this.state.loading) {
this.getPosts();
}
}
This won't be an infinite loop as the getPosts() function sets state loading to false;
Now every time you need an update you just need to set your state loading to true.
If what you want to do is load everytime the jsonUrl updates then you need something like:
componentDidUpdate(prevProps) {
if (prevProps.jsonUrl!== this.props.jsonUrl) {
this.getPosts();
}
}
Also I don't get why you expose your components state by making componentDidMount public.
Modify your getPosts to receive the jsonUrl argument and add the following function to your class:
static getDerivedStateFromProps(props, state) {
if(props.jsonUrl!==state.jsonUrl){
//pass jsonUrl to getPosts
this.getPosts(props.jsonUrl);
return {
...state,
jsonUrl:props.jsonUrl
}
}
return null;
}
You can get rid of the componentDidUpdate function.
You can also remove the getPosts from didmount if you don't set state jsonUrl in the constructor.
// This function runs when a prop choice has been updated
componentDidUpdate(prevProps,prevState) {
// Typical usage (don't forget to compare props):
if (prevState.jsonUrl !== this.state.jsonUrl) {
this.getPosts();
// something else ????
}
}
this way you have to match with the updated state
Try doing this
componentDidUpdate(prevState){
if(prevState.loading!==this.state.loading){
//do Something
this.getPosts();
}}

React-Navigation: Call function whenever page is navigated to

I am developing a React-Native app using React-Navigation, and I am using a stack navigator.
How can I call a function whenever a page is navigated to, including on goBack() events? If I place the method in my constructor, it is only triggered on its initial creation, and not when it is attained through goBack().
use Navigation Events
I believe you can use did focus and will blur
<NavigationEvents
onWillFocus={payload => console.log('will focus', payload)}
onDidFocus={payload => console.log('did focus', payload)}
onWillBlur={payload => console.log('will blur', payload)}
onDidBlur={payload => console.log('did blur', payload)}
/>
https://reactnavigation.org/docs/en/navigation-events.html
EDIT 2022
import { useIsFocused } from '#react-navigation/native';
const isFocused = useIsFocused();
React.useEffect(()=>{
if(isFocused){
// callback
}
},[isFocused])
As you noted the component is never unmounted when changing pages, so you can't rely on the constructor or even componentDidMount. There is a lot of discussion about this topic in this issue.
You could e.g. listen to the didFocus and willBlur events and only render your page when it is focused.
Example
class MyPage extends React.Component {
state = {
isFocused: false
};
componentDidMount() {
this.subs = [
this.props.navigation.addListener("didFocus", () => {
this.setState({ isFocused: true })
}),
this.props.navigation.addListener("willBlur", () => {
this.setState({ isFocused: false })
})
];
}
componentWillUnmount() {
this.subs.forEach(sub => sub.remove());
}
render() {
const { isFocused } = this.state;
if (!isFocused) {
return null;
}
return <MyComponent />;
}
}

Why in the new react-router-dom <Redirect/> does not fire inside the setTimout?

So, I tried to make a little delay between the logout page and redirecting to the main website page. But I fall into the problem, that the react-router-dom <Redirect/> method does not want fire when we put it inside the setTimeout() of setInterval().
So, if we unwrapped it from timer, the <Redirect/> will work normally.
What is the problem is, any suggestions?
My code:
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import axios from 'axios';
class LogoutPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false
}
}
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(
this.setState({
navigate: true
}),
)
.catch(err => {
console.error(err);
});
if (this.state.navigate) {
setTimeout(() => {
return <Redirect to="/employers" />
}, 2000);
}
};
render() {
if (this.state.navigate) {
setTimeout(() => {
return <Redirect to="/employers" />
}, 2000);
}
return (
<div>You successfully logouted</div>
)
}
}
export default LogoutPage;
You want the render() method to return <Redirect /> in order for the redirect to take place. Currently the setTimeout function returns a <Redirect />, but this does not affect the outcome of the render() itself.
So instead, the render() should simply return <Redirect /> if this.state.navigate is true and you delay the setState({ navigate: true }) method with 2 seconds.
Here's a corrected, declarative way of doing it:
class LogoutPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false
}
}
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(() => setTimeout(() => this.setState({ navigate: true }), 2000))
.catch(err => {
console.error(err);
});
};
render() {
if (this.state.navigate) {
return <Redirect to="/employers" />
}
return (
<div>You successfully logouted</div>
);
}
}
For the imperative version, see #Shubham Khatri's answer.
Redirect needs to be rendered for it to work, if you want to redirect from an api call, you can use history.push to programmatically navigate. Also you would rather use setState callback instead of timeout
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(
this.setState({
navigate: true
}, () => {
this.props.history.push("/employers");
}),
)
.catch(err => {
console.error(err);
});
};
Check Programmatically Navigate using react-router for more details
Even if you add Redirect within setTimeout in render it won't work because it will not render it, rather just be a part of the return in setTimeout

Canceling mobx autorun function on componentWillUnmount

I have the following autorun function in my componentDidMount:
componentDidMount() {
this.autoUpdate = autorun(() => {
this.setState({
rows: generateRows(this.props.data)
})
})
}
The problem is that another component changes this.props.data when the component is not mount - and therefor I get the .setState warning on an unmounted component.
So I would like to remove the autorun once the component unmounts.
I tried doing:
componentWillUnmount() {
this.autoUpdate = null
}
But the autorun function still triggers. Is there a way to cancel the mobx autorun once the component is not mounted any more?
autorun returns a disposer function that you need to call in order to cancel it.
class ExampleComponent extends Component {
componentDidMount() {
this.autoUpdateDisposer = autorun(() => {
this.setState({
rows: generateRows(this.props.data)
});
});
}
componentWillUnmount() {
this.autoUpdateDisposer();
}
render() {
// ...
}
}

Categories