React: Docs - Recommendations for isMounted() alternative

Created on 15 Mar 2015  路  13Comments  路  Source: facebook/react

The docs at http://facebook.github.io/react/docs/component-api.html give a use case for isMounted() and mention that it is not available for ES6 classes, but don't explicitly call it out as deprecated.

It would be useful if they clarified what the recommended alternative is.

Most of the uses I have in my existing code are for dealing with the case where a component gets unmounted before a timeout setup for animation etc. after the component is mounted expires.

Most helpful comment

My current hack for this:

/* ... */
componentDidMount() {
    this.mounted = true;
}

componentWillUnmount() {
    this.mounted = false;
}
/* ... */

Forgive me for my sins but it works

All 13 comments

I think the recommended way is to make sure you're properly cleaning up resources and canceling timers, etc. in componentWillUnmount, so that you don't need to worry later about whether the component is still mounted. @sebmarkbage can probably say more.

The common case where this is difficult is for Promises (terrible design to not make them cancelable) but we'll probably add some custom workaround for Promises, for everything else, we recommend that you clean up and timers during componentWillUnmount.

I want to guard async setState call, that sometimes is called before the component is mounted, how can I do that?
Im using ES6

I'd love to see a solution for the Promises issue. We use Promises in our app's database layer, so some of our React components do things (roughly) like this:

componentDidMount: ->
  Database.findById(Draft, {id: @props.draftId}).then (draft) =>
     @setState({draft})

We could certainly create a Flux store, giving us a synchronous subscribe / unsubscribe layer in between the React component and the data being fetched, but it's a lot of boilerplate for a very common pattern.

One solution that would be interesting would be to use Promise chaining:

checkMounted: ->
  if "@isMounted()"
    Promise.resolve(arguments...)
  else
    Promise.reject()

componentDidMount: ->
  Database.findById(Draft, {id: @props.draftId}).then(@checkMounted).then (draft) =>
     @setState({draft})

Another solution we've thought about is wrapping our Promises in a tiny class that makes them "subscribable". Very much defeats the point of Promise syntax, but we could say:

class Unpromisifiy extends EventEmitter
  constructor: (promise): ->
    promise.then => @emit('then', arguments...)
    promise.reject => @emit('reject', arguments...)

...

componentDidMount: ->
  @unpromise = new Unpromisifiy(Database.findById(Draft, {id: @props.draftId}))
  @unpromise.on('then' @_onData)

componentWillUnmount: ->
  @unpromise.off('then', @_onData) if @unpromise

_onData: =>
   @setState({draft})

+1 to @franleplant question.

I'm experimenting with some code to handle the uncancellable promises issue. My (not thoughly tested) solution is to wrap the callback, like so:

componentDidMount() {
    this.ifMounted = initIfMounted();
},

componentWillUnmount() {
    this.ifMounted.unmount();
},

onSearch() {
    return ajax(/* ... */)
        .then(this.ifMounted(response => this.setState(response)));
},

Notice the callback in onSearch is wrapped by ifMounted. That code looks like this:

function initIfMounted() {

    let callbacks = {};
    let count = 0;

    const forId = id => function(...params) {
        const raceSafeCallbacks = callbacks;

        if (raceSafeCallbacks) {
            const callback = raceSafeCallbacks[id];
            delete raceSafeCallbacks[id];
            callback(...params);
        }
    };

    const ifMounted = (callback) => {
        const raceSafeCallbacks = callbacks;

        if (raceSafeCallbacks) {
            const id = count++;
            raceSafeCallbacks[id] = callback;
            return forId(id);
        } else
            return () => {};
    };

    ifMounted.unmount = () => callbacks = null;
    return ifMounted;
}

Basically, it keeps a bag of pending callbacks, which is nullified on unmount, which should dereference them. When any pending promises finally finish, the callback it was given will just see the null callbacks bag and then do nothing.

Disclaimer: I just whipped this up and haven't thoroughly tested it yet. Feel free to use it, break it, and tell me why it doesn't work.

My current hack for this:

/* ... */
componentDidMount() {
    this.mounted = true;
}

componentWillUnmount() {
    this.mounted = false;
}
/* ... */

Forgive me for my sins but it works

Similar to @glittershark but I prefer using state, found this useful when using require.ensure which is only available on client side when running isomorphic applications.

class ... extends Component {

  constructor(props){
    super(props);
    this.state = { mounted: false };
  }

  componentDidMount(){
    this.setState({ mounted: true });
  }

  render(){
    var { mounted } = this.state;
    if(mounted){
        ...
    }
    ...
  }
}

An assertMounted Promise-returning function would be a great help, but I'm not convinced this should live inside React or even Addons.

Implementing this in userland seems like the best choice - then we could benefit from Promise libraries like Bluebird that actually do implement cancelable promises.

I'm currently using a similar approach to @jimbolla where the fulfillment handler is wrapped in a proxy function which passes its arguments onto the original callback unless it is canceled, in which case the proxy becomes a no-op. The proxy is canceled on unmount: https://gist.github.com/robertknight/857d263dd4a68206da79

I ended up using @glittershark 's approach, too. My state was not being updated. Here is the code I had:

export default class CheckBoxDropDown extends React.Component {
  constructor() {
    super()

    this.state = {
      toggleState: false,
      mounted: false
    }
  }

  componentDidMount() {
    this.setState({ mounted: true })
    window.addEventListener('mousedown', this.pageClick.bind(this), false);
  }

  componentWillUnmount() {
    this.setState({ mounted: false })
    window.removeEventListener('mousedown', this.pageClick.bind(this), false)
  }

  pageClick(e) {
    if (!this.state.mounted) return

    let _t = e.target;
    if(!_t.classList.contains(`${this.props.className}list`) && _t.innerHTML != this.props.thisTitle) {
      if(!this.checkClickedObject(_t.offsetParent) && !this.checkClickedObject(_t.offsetParent.offsetParent)) {
          this.setState({"toggleState": false});
      }
    }
  }

  checkClickedObject(theTarget){
    return (theTarget && theTarget.classList.contains(`${this.props.className}list`));
  }

And here's what I changed it to:

export default class CheckBoxDropDown extends React.Component {
  constructor() {
    super()

    this.state = {
      toggleState: false
    }

    this.mounted = false
  }

  componentDidMount() {
    this.mounted = true
    this.toggleEventListener()
  }

  componentWillUnmount() {
    this.mounted = false
    this.toggleEventListener()
  }

  toggleEventListener() {
    let fn = this.pageClick.bind(this)

    if (this.mounted)
      window.addEventListener('mousedown', fn, false)
    else
      window.removeEventListener('mousedown', fn, false)
  }

  handleToggle() {
    if (!this.state.toggleState)
      this.setState({'toggleState': true})
  }

  handleClick() {
    this.setState({'toggleState': !this.state.toggleState})
  }

  pageClick(e) {
    if (!this.mounted) return

    if (this.shouldDisableToggle(e.target))
      this.setState({'toggleState': false})
  }

  shouldDisableToggle(target) {
    return (target.innerHTML != this.props.thisTitle &&
            !this.checkClickedObject(target) &&
            !this.checkClickedObject(target.offsetParent) &&
            !this.checkClickedObject(target.offsetParent.offsetParent))
  }

  checkClickedObject(target){
    return target && target.classList.contains(this.classListName())
  }

  classListName() {
    return `${this.props.className}list`
  }

@hectron I see a few issues with this approach:

  • If you're properly adding and removing the event listener at CDM and CWU, there should be no need for an isMounted check.
  • Your use of bind is preventing the reference check on window.removeEventListener from succeeding. There is a simple fix, if you're using ES7 property initializers:
class Foo extends React.Component {

  componentDidMount() {
    window.addEventListener('mousedown', this.pageClick, false);
  }

  componentWillUnmount() {
    window.removeEventListener('mousedown', this.pageClick, false);
  }

  // Use a combination of a property initializer and an arrow function to bind!
  // Compiles to, roughly:
  // `_this = this; this.pageClick = function(e) { if (_this.shouldDisableToggle ...`
  pageClick = (e) => {
    if (this.shouldDisableToggle(e.target)) { /* ...  */ }
  };

}

Otherwise, in the constructor:

  constructor(props, context) {
    super(props, context);
    this.pageClick = this.pageClick.bind(this);
  }

Tracking the cancellable promise seems fairly verbose, and it forces adding some state to the component which seems undesirable (keeping a reference to the promise so you can cancel it in componentWillUnmount), which seems equivalent to manually tracking the state as described above.

It would be nice if the logic could be self-contained similar to cancellable observables:

componentDidMount() {
  makeCancelable( doAsyncThing() )
    .takeUntil(this.componentUnmounted)
    .then (results) => {
      this.setState(results)
    }
}

But I'm not sure how React would generate the unmounted signal for that to work.

Was this page helpful?
0 / 5 - 0 ratings