Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate `isMounted` #5465

Closed
jimfb opened this issue Nov 13, 2015 · 48 comments
Closed

Deprecate `isMounted` #5465

jimfb opened this issue Nov 13, 2015 · 48 comments

Comments

@jimfb
Copy link
Contributor

@jimfb jimfb commented Nov 13, 2015

isMounted is already unavailable on ES6 classes, and we already have a warning saying we "might" remove them, but we don't actually have a github issue to deprecate them. As per our discussions today, we are basically agreed that we're going to start moving away from isMounted and deprecate it. We still need to figure out some good stories around promises (and related use cases).

This issue is to track progress toward that goal.

For background, please read:

@probablyup

This comment has been minimized.

Copy link
Contributor

@probablyup probablyup commented Nov 16, 2015

I don't agree with this. ES6 promises in particular cannot be reliably cancelled on componentWillUnmount, so removing the only way to check if the component is mounted before setState or another action is opening the way for a lot of hard to trace async bugs.

@jimfb

This comment has been minimized.

Copy link
Contributor Author

@jimfb jimfb commented Nov 16, 2015

@yaycmyk Thus the line:

We still need to figure out some good stories around promises (and related use cases).

Please read the background issues I listed, in particular: #2787 (comment)

@probablyup

This comment has been minimized.

Copy link
Contributor

@probablyup probablyup commented Nov 16, 2015

I did read the comments. I just find the issues intractable.

@nvartolomei

This comment has been minimized.

Copy link

@nvartolomei nvartolomei commented Nov 16, 2015

Why promises cant be reliable cancelled? Any sources/proofs/examples?

On Monday, November 16, 2015, Evan Jacobs notifications@github.com wrote:

I don't agree with this. ES6 promises in particular cannot be reliably
cancelled on componentWillUnmount, so removing the only way to check if
the component is mounted before setState or another action is opening the
way for a lot of hard to trace async bugs.

@probablyup

This comment has been minimized.

Copy link
Contributor

@probablyup probablyup commented Nov 16, 2015

@nvartolomei Look at the ES6 promise spec.

@zpao

This comment has been minimized.

Copy link
Member

@zpao zpao commented Nov 16, 2015

This is a longer term goal, not something that is happening immediately. But we want to track the planning and discussions in a single place and not across comments in every issue when this comes up. We are aware of the problem of Promises currently being uncancellable which is a major reason we haven't already done this.

@jimfb

This comment has been minimized.

Copy link
Contributor Author

@jimfb jimfb commented Nov 16, 2015

@yaycmyk To over-simplify a very complex issue... the comments are saying... using isMounted to avoid setState for unmounted components doesn't actually solve the problem that the setState warning was trying to indicate - in fact, it just hides the problem. Also, calling setState as the result of a promise is a bit of an anti-pattern anyway, since it can cause race conditions which won't necessarily show up in testing. Thus we want to get rid of it, and figure out a "best practice" recommendation for using promises with React.

I agree the issues are a bit inscrutable, but that's largely because it's a complex issue that we're still figuring out and don't yet have a canned response for.

@probablyup

This comment has been minimized.

Copy link
Contributor

@probablyup probablyup commented Nov 16, 2015

calling setState as the result of a promise is a bit of an anti-pattern anyway, since it can cause race conditions which won't necessarily show up in testing

We can agree to disagree on that one. There are times when content is being fetched asynchronously and you don't want to have to go through a full-scale rerender to pop that content in once it is resolved. I use it specifically in an infinite table view implementation where a full virtual rerender would be unnecessary.

@jimbolla

This comment has been minimized.

Copy link

@jimbolla jimbolla commented Nov 16, 2015

You might not be able to cancel a promise, but you can make it dereference the component on unmount, like so:

const SomeComponent = React.createClass({
    componentDidMount() {
        this.protect = protectFromUnmount();

        ajax(/* */).then(
            this.protect( // <-- barrier between the promise and the component
                response => {this.setState({thing: response.thing});}
            )
        );
    },
    componentWillUnmount() {
        this.protect.unmount();
    },
});

The important distinction is when this.protect.unmount() is called in componentWillUnmount, all callbacks get dereferenced, meaning the component gets dereferenced, and then when the promise completes, it just calls a no-op. This should prevent any memory leaks related to promises references unmounted components. source for protectFromUnmount

@istarkov

This comment has been minimized.

Copy link
Contributor

@istarkov istarkov commented Nov 18, 2015

This simple method can be used to add cancel to any promise

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then((val) =>
      hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
    );
    promise.catch((error) =>
      hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

EDIT: Updated for correctness/completeness.

HOW TO USE

const somePromise = new Promise(r => setTimeout(r, 1000));

const cancelable = makeCancelable(somePromise);

cancelable
  .promise
  .then(() => console.log('resolved'))
  .catch(({isCanceled, ...error}) => console.log('isCanceled', isCanceled));

// Cancel promise
cancelable.cancel();
@probablyup

This comment has been minimized.

Copy link
Contributor

@probablyup probablyup commented Nov 18, 2015

Listing ways to sup-up ES6 promises to make them cancellable is besides the point. The intent should be to provide a solution that works WITH the spec rather than trying to work AROUND the spec.

@ir-fuel

This comment has been minimized.

Copy link

@ir-fuel ir-fuel commented Nov 20, 2015

I agree. Instead of simply checking if the component is still mounted when we receive the promise result we have to resort to all kinds of magic so we can "unbind" our promise from the component it's supposed to set its result in, clearly fighting against the way promises are designed.
To me it feels like overengineering a solution where a simple test is the easiest way to take care of this.

@lasekio

This comment has been minimized.

Copy link
Contributor

@lasekio lasekio commented Nov 20, 2015

We can keep simple checking just by:

React.createClass(function() {
  componentDidMount: function() {
    this._isMounted = true;

    ajax(/* */).then(this.handleResponse);
  }

  handleResponse: function(response) {
    if (!this._isMounted) return; // Protection

    /* */
  }

  componentWillUnmount: function() {
    this._isMounted = false;
  }
});
@ir-fuel

This comment has been minimized.

Copy link

@ir-fuel ir-fuel commented Nov 20, 2015

This is of course my opinion, but it seems to me that async data loading with a promise inside a react component is such a common scenario that it should be covered by react, instead of having to write our own boilerplate code.

@lasekio

This comment has been minimized.

Copy link
Contributor

@lasekio lasekio commented Nov 20, 2015

The problem is, that to fallow the true mount state we must add listener when react will finish DOM mount process in each component (the same, which attach componentDidMount, if defined), but it will affect on perf, because we don't need to fallow it everywhere. Component dont listen DOM mount ready by default since componentDidMount is undefined.

@BurntCaramel

This comment has been minimized.

Copy link

@BurntCaramel BurntCaramel commented Dec 7, 2015

What if setState could be passed a chained promise which resolves to the desired state changes? If the component unmounts, then if there are any pending promises their eventual result is ignored.

@koistya

This comment has been minimized.

Copy link
Contributor

@koistya koistya commented Dec 17, 2015

@istarkov nice pattern, like it! Here is slightly altered API for it:

// create a new promise
const [response, cancel] = await cancelable(fetch('/api/data'));

// cancel it
cancel();
@dtertman

This comment has been minimized.

Copy link

@dtertman dtertman commented Jan 19, 2016

Since I'm new to React and reading docs, just to throw this out there : the Load Initial Data via Ajax tip uses .isMounted(), so the website disagrees with the website. It would be great to see a complete Tip about how to cancel the initial load in componentWillUnmount, maybe using @istarkov's pattern above.

@jimfb

This comment has been minimized.

Copy link
Contributor Author

@jimfb jimfb commented Jan 19, 2016

@dtertman Fixed in #5870, will be online when the docs get cherry-picked over.

@dtertman

This comment has been minimized.

Copy link

@dtertman dtertman commented Jan 19, 2016

@jimfb thanks, not sure how I missed that in search.

@vpontis

This comment has been minimized.

Copy link

@vpontis vpontis commented Mar 1, 2016

@istarkov not sure if this was intentional but your makeCancelable does not handle if the original promise fails. When the original promise is rejected, no handler gets called.

This does not seem ideal because you may still want to handle an error on the original promise.

Here is my proposal for a makeCancelable that handles a rejection in the original promise:

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then((val) =>
      hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
    );
    promise.catch((error) =>
      hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

I'm not sure where I stand on if making cancelable promises is a good idea, but if we are going to make promises cancelable, we should preserve the underlying behavior :).

@istarkov

This comment has been minimized.

Copy link
Contributor

@istarkov istarkov commented Mar 1, 2016

@vpontis 👍

@vpontis

This comment has been minimized.

Copy link

@vpontis vpontis commented Mar 1, 2016

@istarkov your original post is referenced here: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

Want to update your post or should I message the author of the post?

@jimfb

This comment has been minimized.

Copy link
Contributor Author

@jimfb jimfb commented Mar 1, 2016

@vpontis Thanks, I'll fix! (#6152)

@mo

This comment has been minimized.

Copy link

@mo mo commented Jul 24, 2017

A few days ago a new API was added to the DOM specification that allows you to abort fetch() requests. This API is not implemented in any browser yet but I have created a polyfill for it available on NPM was "abortcontroller-polyfill". The polyfill does essentially the same thing as the code posted by @istarkov but allows you to transition with no code changes to the real browser API once it's implemented.

Details here:
https://mo.github.io/2017/07/24/abort-fetch-abortcontroller-polyfill.html

@aweary

This comment has been minimized.

Copy link
Member

@aweary aweary commented Sep 20, 2017

Since React.createClass no longer exists in React 16 and the new create-react-class package includes a clear deprecation message for isMounted, I'm going to close this out.

@aweary aweary closed this Sep 20, 2017
@hjylewis

This comment has been minimized.

Copy link

@hjylewis hjylewis commented Oct 26, 2017

I agree with @benmmurphy that @istarkov's solution is effectively the same as using isMounted() since it doesn't solve the garbage collection problem.

@benmmurphy's solution is closer but nulls out the wrong variables so the promise handlers are not dereferenced.

The key is passing a function up through the closure that dereferences the handlers:

const makeCancelable = promise => {
  let cancel = () => {};

  const wrappedPromise = new Promise((resolve, reject) => {
    cancel = () => {
      resolve = null;
      reject = null;
    };

    promise.then(
      val => {
        if (resolve) resolve(val);
      },
      error => {
        if (reject) reject(error);
      }
    );
  });

  wrappedPromise.cancel = cancel;
  return wrappedPromise;
};

Further explanation about why this solution allows for garbage collection and not the previous solutions can be found here.

I went ahead and turned this into an npm package, trashable. And since the use case is react, I made a HOC component that tracks promises and cancels them when the component gets unmounted, trashable-react.

@pygy

This comment has been minimized.

Copy link

@pygy pygy commented Nov 10, 2017

Edit: my bad, I just looked at @hjylewis thrashable, and it does cancel promises as well. Still the pattern below is IMO a small improvement.

None of these solutions cancel Promises, which can be cancelled without any extension by absorbing a forever-pending promise.

function makeCancelable(promise) {
  let active = true;
  return {
    cancel() {active = false},
    promise: promise.then(
      value => active ? value : new Promise(()=>{}),
      reason => active ? reason : new Promise(()=>{})
    )
  }
}

// used as above:

const {promise, cancel} = makeCancelable(Promise.resolve("Hey!"))

promise.then((v) => console.log(v)) // never logs
cancel()

live here

There may be subtleties to iron out regarding GC and coffee has yet to kick in, but that pattern ensures that the promise returned is really cancelled and it can be made not to leak (I've implemented it in the past).

@hjylewis

This comment has been minimized.

Copy link

@hjylewis hjylewis commented Nov 10, 2017

@pygy Thanks for the reply!

Unfortunately, your solution still doesn't allow for Garbage Collection. You've essentially just rewritten @istarkov's solution which uses a conditional.

You can test this easily by dropping this implementation into trashable and running the tests (the garbage collection test fails).

Your implementation also fails to properly handle errors.

@adeelibr

This comment has been minimized.

Copy link

@adeelibr adeelibr commented Jul 5, 2018

It's 2018 is there an even better approach then the one's mentioned above?

@npblubb

This comment has been minimized.

Copy link

@npblubb npblubb commented Jul 5, 2018

yes you can use some navigation frameworks that have a documentation twice the size of react native but is very professionel

@dantman

This comment has been minimized.

Copy link
Contributor

@dantman dantman commented Feb 5, 2019

These snippets for "canceling" a promise aren't that great IMHO. The cancelled promises will still not resolve until the original promise resolves. So memory cleanup won't happen till it would if you just used an isMounted trick.

A proper cancelable promise wrapper would have to make use of a second promise and Promise.race. i.e. Promise.race([originalPromise, cancelationPromise])

@benmmurphy

This comment has been minimized.

Copy link

@benmmurphy benmmurphy commented Feb 6, 2019

@benmmurphy's solution is closer but nulls out the wrong variables so the promise handlers are not dereferenced.

I think my solution works but I don't know enough about what promises the javascript runtime gives to know for sure. If you run the solution under node in your test harness it correctly GCs the value. My solution assigned the the resolve/reject functions to a higher scope and then nulled these values out when cancel was called. However, the functions were still available in the lower scope but not referenced. I think modern javascript engines don't capture variables in a closure unless they are referenced. I think this used to be a big problem where people would accidentally create DOM leaks because they did stuff like: var element = findDOM(); element.addEventListener('click', function() {}); and element would be referenced in the closure even though it wasn't used in the closure.

@drprasad-capillary

This comment has been minimized.

Copy link

@drprasad-capillary drprasad-capillary commented Feb 25, 2019

@hjylewis @benmmurphy why do we need to dereference handlers ?? after handlers excuted, garbage collection any way happens, right ??

@benmmurphy

This comment has been minimized.

Copy link

@benmmurphy benmmurphy commented Feb 25, 2019

These snippets for "canceling" a promise aren't that great IMHO. The cancelled promises will still not resolve until the original promise resolves. So memory cleanup won't happen till it would if you just used an isMounted trick.

A proper cancelable promise wrapper would have to make use of a second promise and Promise.race. i.e. Promise.race([originalPromise, cancelationPromise])

@hjylewis and mine do you actually work you can verify it with node weak. but looking at them again i agree neither of them are idiosyncratic written promise code. as a promise user you would probable expect a 'cancelled' promise to resolve in the rejected state and neither of them do this. though, possibly in the case of a component this is a solution that would be easier to use because you don't have to write extra code to ignore the reject handler.

i think an idiosyncratic rejectable promise would use Promise.race([]) to build a cancellable promise. it works because when a promise becomes resolved the pending callbacks are deleted so at the point there would be no reference chain from the browser network to your component because there would be no longer a reference between the race promise and the component.

@ketysek

This comment has been minimized.

Copy link

@ketysek ketysek commented Mar 1, 2019

I'm curious if it's somehow possible to use Promise.all() with those cancelable promises and avoid uncaught errors in browsers console ... because I'm able to catch only first cancellation error, others remains uncaught.

@adeelibr

This comment has been minimized.

Copy link

@adeelibr adeelibr commented May 17, 2019

It's 2018 is there an even better approach then the one's mentioned above?

Any better approach to cancel a promise execution i.e, setTimeout, API Calls etc.. It's 2019 😭 😞

@adeelibr

This comment has been minimized.

Copy link

@adeelibr adeelibr commented May 17, 2019

There is Promise cancellation thread going on TC39, (I think) it's of relevance here (maybe .. not sure)
tc39/proposal-cancellation#24

@paul4156

This comment has been minimized.

Copy link

@paul4156 paul4156 commented Sep 18, 2019

Any better approach to cancel a promise execution i.e, setTimeout, API Calls etc.. It's 2019 😭 😞

Are we looking for something like

const promise = new Promise(r => setTimeout(r, 1000))
  .then(() => console.log('resolved'))
  .catch(()=> console.log('error'))
  .canceled(() => console.log('canceled'));

// Cancel promise
promise.cancel();
filipemir added a commit to filipemir/reactjs.org that referenced this issue Nov 12, 2019
As discussed in [this thread](facebook/react#5465 (comment)) and expanded [here](https://github.com/hjylewis/trashable/blob/master/PROOF.md), the solution proposed for cancelling promises when a component unmounts doesn't actually address the possible memory leaks. Interestingly, I don't believe @hjylewis's approach does so fully either (see [this issue](hjylewis/trashable#6)) but it does so in the vast majority of cases. If I'm following the conversation along correctly, I think the docs should be updated to reflect the better answer. And if it's not the better answer, I would love to hear the arguments against it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.