I am coding a simple app on reactjs-flux and everything works fine except I am receiving a warning from reactjs telling me that I am calling setState on unmounted components
So I have found the solution, it turns out I only had to assign this._onChange.bind(this)
to an internal property before passing it as an argument to removechangelistener
and addchangelistener
. Here is the solution:
componentDidMount() {
this.changeListener = this._onChange.bind(this);
ProductsStore.addChangeListener(this.changeListener);
this._showProducts();
}
componentWillUnmount() {
ProductsStore.removeChangeListener(this.changeListener);
}
I do not know, however, why this solves the issue. Any ideas?
I decided it so
class Tooltip extends React.Component {
constructor (props) {
super(props);
this.state = {
handleOutsideClick: this.handleOutsideClick.bind(this)
};
}
componentDidMount () {
window.addEventListener('click', this.state.handleOutsideClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.state.handleOutsideClick);
}
}
This is a es6 problem. React.createClass binds 'this' properly for all function defined inside its scope.
For es6, you have to do something yourself to bind the right 'this'. Calling bind(this) however, creates a new function each time, and passing its return value to removeChangeListener won't match the function passed into addChangeListener created by an earlier bind(this) call.
I see one solution here where bind(this) is called once for each function and the return value is saved and re-used later. That'll work fine. A more popular and slightly cleaner solution is using es6's arrow function.
componentDidMount() {
ProductsStore.addChangeListener(() => { this._onChange() });
}
componentWillUnmount() {
ProductsStore.removeChangeListener(() => { this._onChange());
}
Arrow functions capture the 'this' of the enclosing context without creating new functions each time. It's sort of designed for stuff like this.
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the exports component.
I am using the exact same implementation across multiple react components. i.e. this is repeated across several .jsx components.
componentDidMount: function() {
console.log('DidMount- Component 1');
ViewStateStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
console.log('DidUnMount- Component 1');
ViewStateStore.removeChangeListener(this._onChange);
},
_onChange:function()
{
console.log('SetState- Component 1');
this.setState(getStateFromStores());
},
Possible Solution
Currently the following is working out for me, but it has been a little temperamental. Wrap the call back in a function/named-function.
ViewStateStore.addChangeListener(function (){this._onChange});
one might also try
ViewStateStore.addChangeListener(function named(){this._onChange});
Theory
EventEmitter is for some reason getting confused identifying the callback to remove. Using a named function is perhaps helping with that.
Try removing the .bind(this)
from your addChangeListener
and removeChangeListener
. They are already bound to your component when they get called.
As you already got to know the solution here, I will try to explain what's happening.
As per ES5 standard, we used to write following code to add and remove listener.
componentWillMount: function() {
BaseStore.addChangeListener("ON_API_SUCCESS", this._updateStore);
},
componentWillUnmount: function() {
BaseStore.removeChangeListener("ON_API_SUCCESS", this._updateStore);
}
In above code, memory reference for the callback function (ie: this._updateStore) is same. So, removeChangeListener will look for reference and will remove it.
Since, ES6 standard lacks autobinding this
by default you have to bind this
explicitly to the function.
Note: Bind method returns new reference for the callback.
Refer here for more info about bind
This is where problem occurs. When we do this._updateStore.bind(this)
, bind method returns new reference for that function. So, the reference that you have sent as an argument to addChangeListener is not same as the one in removeChangeListener method.
this._updateStore.bind(this) != this._updateStore.bind(this)
Solution:
There are two ways to solve this problem.
1. Store the event handler (ie: this._updateStore)
in constructor as a member variable. (Your solution)
2. Create a custom changeListener function in store that will bind this
for you. (Source: here)
Solution 1 explanation:
constructor (props) {
super(props);
/* Here we are binding "this" to _updateStore and storing
that inside _updateStoreHandler member */
this._updateStoreHandler = this._updateStore.bind(this);
/* Now we gonna user _updateStoreHandler's reference for
adding and removing change listener */
this.state = {
data: []
};
}
componentWillMount () {
/* Here we are using member "_updateStoreHandler" to add listener */
BaseStore.addChangeListener("ON_STORE_UPDATE", this._updateStoreHandler);
}
componentWillUnmount () {
/* Here we are using member "_updateStoreHandler" to remove listener */
BaseStore.removeChangeListener("ON_STORE_UPDATE", this._updateStoreHandler);
}
In above code, we are binding this
to _updateStore function and assigning that to a member inside constructor. Later we are using that member to add and remove change listener.
Solution 2 explanation:
In this method, we modify BaseStore functionalities. Idea is to modify addChangeListener function in BaseStore to receive second argument this
and inside that function we are binding this
to the callback and storing that reference, so that while removing change listener we can remove with that reference.
You can find complete code gist here and source here.