Reputation: 439
Related question here but I am not sure how to adapt the solution to this problem.
I am trying to create a reusable component for a landing page with tabs. Each tab is a child of the reusable component and has its own store defined as a prop:
<LandingPage>
<LandingPage.Tab store={store1}/>
<LandingPage.Tab store={store2}/>
...
<LandingPage.Tab store={storeN}/>
</LandingPage>
I'd like to fetch data from every tab's store when the parent component mounts to allow quick switching between tabs. Inside the componentDidMount function, I iterate over each child and assign the onChange callback for the child's store to an anonymous arrow function:
var LandingPage = React.createClass({
getInitialState: function () {
return {
data: [] /* each index will be an array of data for a different tab */
};
},
componentDidMount: function () {
var self = this;
React.Children.forEach(this.props.children, function (child, index) {
child.props.store.onChange(() => {
self.setDataAtIndex(index, child.props.store.getData());
});
});
},
setDataAtIndex: function (index, newData) {
var data = this.state.data.slice();
data[index] = newData;
this.setState({
data: data
});
},
...
});
However, when the page first loads, I get a warning message from React:
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 LandingPage component.
I am confused because I thought I could assume the component is mounted if I am inside the componentDidMount function. This warning message goes away when I refresh the page.
Can someone explain this behavior and tell me how to correctly structure code to eliminate the warning message?
Upvotes: 3
Views: 935
Reputation: 439
While Charlie Martin's answer is a clever workaround, I ultimately decided to store the callbacks in a state variable, similar to the related question I linked to. Here's the updated code:
var LandingPage = React.createClass({
getInitialState: function () {
return {
data: [] /* each index will be an array of data for a different tab */
callbacks: [] /* each index will store a callback reference */
};
},
componentDidMount: function () {
var self = this;
React.Children.forEach(this.props.children, function (child, index) {
var fn = function() {
self.setDataAtIndex(index, child.props.store.getAll());
};
child.props.store.onChange(fn);
self.saveCallback(index, fn);
});
},
componentWillUnmount: function () {
var self = this;
React.Children.forEach(this.props.children, function (child, index) {
child.props.store.offChange(self.state.callbacks[index]);
});
},
saveCallback: function (index, fn) {
var callbacks = this.state.callbacks;
callbacks[index] = fn;
this.setState({
callbacks: callbacks
});
},
...
});
For reference, onChange() and offChange() are defined as:
function onChange(callback) {
this.on('change', callback);
}
function offChange(callback) {
this.removeListener('change', callback);
}
Upvotes: 1
Reputation: 8406
This function...
() => { // this calls setState
self.setDataAtIndex(index, child.props.store.getData());
}
Will be called every time a tabs store changes whether the LandingPage
component is mounted or not. That is a problem. You need to tell the stores to stop calling this function when LandingPage
unmounts. Without modifying the store, you could just override the change listener with a no-op, like this...
componentWillUnmount: function () {
var self = this;
React.Children.forEach(this.props.children, function (child, index) {
child.props.store.onChange(() => {});
});
}
Now, when the component isn't mounted, () => {}
should be called instead, which doesn't call setState
and is therefore harmless
Upvotes: 1