44

I'm getting an error message due to an async method issue. In my terminal I'm seeing:

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.
- node_modules/fbjs/lib/warning.js:33:20 in printWarning
- node_modules/fbjs/lib/warning.js:57:25 in warning
- node_modules/react-native/Libraries/Renderer/ReactNativeRenderer-dev.js:12196:6 in warnAboutUpdateOnUnmounted
- node_modules/react-native/Libraries/Renderer/ReactNativeRenderer-dev.js:13273:41 in scheduleWorkImpl
- node_modules/react-native/Libraries/Renderer/ReactNativeRenderer-dev.js:6224:19 in enqueueSetState
- node_modules/react/cjs/react.development.js:242:31 in setState
* router/_components/item.js:51:16 in getImage$
- node_modules/regenerator-runtime/runtime.js:62:44 in tryCatch
- node_modules/regenerator-runtime/runtime.js:296:30 in invoke
- ... 13 more stack frames from framework internals

I noticed it's specifically pointing out the getImage$

Here's the code I'm using for that section:

export default class extends Component {
    constructor(props) {
        super(props);
        const { item } = props

        const bindThese = { item }
        this.boundActionCreators = bindActionCreators(bindThese)

        this.state = {
            image: require('../../static/logo.png'),
            ready: false,
            showOptions: this.props.showOptions
        }

        this.getImage = this.getImage.bind(this)
        this.renderNotAdmin = this.renderNotAdmin.bind(this)
        this.renderAdmin = this.renderAdmin.bind(this)
        this.handleOutOfStock = this.handleOutOfStock.bind(this)
    }

    async getImage(img) {
        let imgUri = await Amplify.Storage.get(img)
        let uri = await CacheManager.get(imgUri).getPath()

        this.setState({
            image: { uri },
            ready: true
        })
    }

    componentDidMount() {
        this.getImage(this.props.item.image)
    }

I'm trying to figure out how to use a componentWillUnmount with this async method. How do I go about it?

Thanks!

KARTHIKEYAN.A
  • 18,210
  • 6
  • 124
  • 133
Dres
  • 1,449
  • 3
  • 18
  • 35
  • What is leading to this error? Just loading the component is it? Can you post the `render` method as well? – yaswanth Aug 28 '18 at 15:33

4 Answers4

82

You can use isMounted React pattern to avoid memory leaks here.

In your constructor:

constructor(props) {
    super(props);

    this._isMounted = false;
// rest of your code
}

componentDidMount() {
    this._isMounted = true;
    this._isMounted && this.getImage(this.props.item.image);

}

in your componentWillUnmount

componentWillUnmount() {
   this._isMounted = false;
}

While in you getImage()

async getImage(img) {
    let imgUri = await Amplify.Storage.get(img)
    let uri = await CacheManager.get(imgUri).getPath()

    this._isMounted && this.setState({
        image: { uri },
        ready: true
    })
}

A recommend approach to use Axios which is based cancellable promise pattern. So you can cancel any network call while unmounting the component with it's cancelToken subscription. Here is resource for Axios Cancellation

Greg K
  • 10,770
  • 10
  • 45
  • 62
Sakhi Mansoor
  • 7,832
  • 5
  • 22
  • 37
  • 1
    How do you cancel, if component umounts after network request began and before it ended. – Arup Rakshit Aug 28 '18 at 15:46
  • 1
    xhr request has their own `AbortController`. Axios uses it's token as signal and we pass it in our axios API call. At any moment we can cancel that pending request with it's token by `axios.CancelToken.source().cancel('API is Cacnelled'). It's great and so powerful without leaving any caveats in our app. I have had a great time with it while clear redux store once. But it worked like a charm – Sakhi Mansoor Aug 28 '18 at 15:50
  • @SakhiMansoor You made it very clear! works like a charm, I just added an underline at: this._isMounted && this.getImage(this.props.item.image); – Cassio Seffrin Dec 30 '18 at 02:32
  • 1
    incredible, and thank you for the axios cancellation link. thank you – Canovice Jul 01 '19 at 19:18
  • 1
    But how can I do this if I'm using React Hooks? I have written this: https://hastebin.com/omihuhurag.js but it still gives me the same error. :( – Ishita Sinha Nov 06 '19 at 11:03
  • @IshitaSinha I can look into this and would try to come up with the solution. Thanks for bringing this up – Sakhi Mansoor Nov 11 '19 at 11:27
  • Any luck so far? – Ishita Sinha Nov 28 '19 at 10:57
  • I didn't get much time to look into this. thanks for reminder, I would definitely look into this. Thanks for the patience – Sakhi Mansoor Nov 28 '19 at 12:53
  • variable name without an underscore will not work... strange? – Akber Iqbal Jan 24 '20 at 01:49
  • 1
    Hello! I found this on https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html. It seems is anti-pattern or your case is different? – Dastan Akhmetov Mar 17 '21 at 09:31
9

From the React blog

Just set a _isMounted property to true in componentDidMount and set it to false in componentWillUnmount, and use this variable to check your component’s status.

It goes on to say that ideally, this would instead be fixed by using cancellable callbacks, although the first solution seems suitable here.

What you definitely shouldn't do is use the isMounted() function, which may be deprecated.

Keir
  • 752
  • 5
  • 8
5

You need to set this.mounted = false in componentWillUnmount() method and this.mounted = true in componentDidMount() method.

The setState update is conditional based need to declare in componentDidMount() method.

componentDidMount() {
        this.mounted = true;
        var myVar =  setInterval(() => {
                let nextPercent = this.state.percentage+10;
                if (nextPercent >= 100) {
                    clearInterval(myVar);
                }
                if(this.mounted) {
                    this.setState({ percentage: nextPercent });
            }
        }, 100);
}

componentWillUnmount(){
      this.mounted = false;
}
KARTHIKEYAN.A
  • 18,210
  • 6
  • 124
  • 133
0

I resolved the error by overriding the setState method

isSubscribed = true;

componentWillUnmount() {
  this.isSubscribed = false;
}

setState = (state, callback?) => {
   if (this.isSubscribed) {
     super.setState(state, callback);
   }
}
dyliu
  • 1