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.
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:
isMounted
check.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.
Most helpful comment
My current hack for this:
Forgive me for my sins but it works