简体   繁体   中英

“setState(…): Can only update a mounted or mounting component” inside componentDidMount

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?

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

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);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM