ReactJS: "Can't perform a React state update on an unmounted component" when open dialog

1k Views Asked by At

I got an error when open dialog from another class component: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method"

index.js

import ...

class AdMenu extends Component {
    componentWillMount = () => {
        this.onSearch();
    };

    onOpenInsert = () => {
        showDetailDialog();
    };

    onSearch = () => {
        fetch(_url, ...)
            .then(response => {
                if (response.ok) {
                    return response.json();
                } else {
                    throw response;
                }
            })
            .then(responseJson => {
                this.setState({...});
            })
            .catch(response => {...});
    };

    render() {
        return (
            <div>
                <DetailDialog />
                <Button color="primary" onClick={this.onOpenInsert}>Add New</Button>
                <BootstrapTable .... />
            </div>
        );
    }
}

export default withTranslation()(AdMenu);

DetailDialog.js


export var showDetailDialog = function () {
    this.setState({open: true});
    console.log('Mounted: ' + this.mounted);
};

class DetailDialog extends React.Component {
    mounted = false;
    controller = new AbortController();
    constructor(props) {
        super(props);
        this.state = {open: false};
        showDetailDialog = showDetailDialog.bind(this);
    }
    componentDidMount() {
        console.log('componentDidMount');
        this.mounted = true;
    }

    componentWillUnmount(){
        console.log('componentWillUnmount');
        this.mounted = false;
    }
    onClose = () => {
        this.setState({open: false});
    };

    render() {
        return (
            <Modal isOpen={this.state.open} toggle={this.onClose} className={"modal-primary"} >
                <ModalHeader toggle={this.onClose}>Detail</ModalHeader>
                <ModalBody>
                    ...
                </ModalBody>
            </Modal>
        );
    }
}

export default withTranslation()(DetailDialog);

I have a DetailDialog exported class component and function showDetailDialog. It imported to index.js page.

When I open page in the first time and click open dialog then work fine. But when I switch to another page by Router in menu then open again page in the second time, I got an error in console log.

I tried use this.mounted var to check unmounted component, but I don't know How to set state to open detail dialog when component had unmount in the second time and next.

I tried use controller = new AbortController(); and controller.abort() in componentWillUnmount() but not working.

Or any solution for this problem?

Thanks!

Image: https://prnt.sc/nsp251

error image in console log

Source on CodeSandbox: https://codesandbox.io/s/coreuicoreuifreereactadmintemplate-5unwj

Step test:

  • Click Ad Menu (1 st)

  • Click Ad Group

  • Click Ad Menu (2 nd)

  • Click Open Dialog in Ad Menu

  • View Console log browser

File: src/views/category

Node v11.12.0

Npm 6.7.0

Window 10

2

There are 2 best solutions below

4
sachin kalekar On

Move the logic that you have written in componentWillMount to componentDidMount in AdMenu

3
Rallen On

Your problem is the usage of an external function showDetailDialog to access the state of the DetailDialog component. The function that is used in your AdMenu component and the function bound to the DetailDialog component in its constructor are not one and the same.

A solution would be to use Refs and expose an open function on the component itself.

class DetailDialog extends Component {
    open = () => this.setState({ open: true });
}

/* ... */

class AdMenu extends Component {
    constructor(props) {
        super(props);
        this.detailDialog = React.createRef();
        this.onOpenInsert = this.onOpenInsert.bind(this);
    }

    onOpenInsert() {
        this.detailDialog.current.open();
    }

    render() {
        return (
            <DetailDialog ref={this.detailDialog} />
            { ... }
        );
    }
}

But this approach is not recommended by the React documentation for Refs.

There are a few good use cases for refs:

  • Managing focus, text selection, or media playback.
  • Triggering imperative animations.
  • Integrating with third-party DOM libraries.

Avoid using refs for anything that can be done declaratively.

For example, instead of exposing open() and close() methods on a Dialog component, pass an isOpen prop to it.

Per recommendation of the documentation you could declare a detailOpen state on the AdMenu component and pass that down to the DetailDialog component as an open prop.

class AdMenu extends Component {
    constructor(props) {
        super(props);
        this.state = {
            detailOpen: false
        }
    }

    onOpenInsert() {
        this.setState({ detailOpen: true });
    }
    
    onDialogClose() {
        this.setState({ detailOpen: false });
    }
    
    /* ... */

    render() {
        return (
            <DetailDialog open={this.state.detailOpen} onClose={this.onDialogClose} />
            { ... }
        );
    }
}

/* ... */

class DetailDialog extends Component {
    /* ... */

    render() {
        return (
            <Modal isOpen={this.props.open} toggle={this.props.onClose}>
                <ModalHeader toggle={this.props.onClose}>Detail</ModalHeader>
                <ModalBody>
                    ...
                </ModalBody>
            </Modal>
        );
    }
}

What approach you choose is up to you.