0

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

  • Use `componentDidMount()` instead of `componentWillMount()` and check if the issue is still present. BTW, always avoid `componentWillMount()`. – Igor Soloydenko May 24 '19 at 08:01
  • Also, related: https://stackoverflow.com/a/53949849/482868 I think, you should wrap your `this.setState()` into `if`s which check whether the component is still mounted or not. Does it make sense? – Igor Soloydenko May 24 '19 at 08:06
  • Thanks Igor Soloydenko!, I tried componentWillMount() { this.mounted = true; } but still error because has something not mount again when didMount or something not cancel when willUnmount. I don't know what is that :| – Tùng Huynh May 24 '19 at 08:11
  • I tried too if (this.mounted) { this.setState({ open: true }); }else{//what to do?}. It no error and not show dialog, too – Tùng Huynh May 24 '19 at 08:14
  • @IgorSoloydenko: Or Is there another way to open the detail dialog from the index.js without export the function showDetailDialog ? – Tùng Huynh May 24 '19 at 09:22

2 Answers2

0

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

sachin kalekar
  • 526
  • 2
  • 9
0

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.

Community
  • 1
  • 1
Rallen
  • 2,240
  • 20
  • 22
  • Many thanks!. The first, I used detailOpen state in AdMenu, but when I need pass data from AdMenu to DetailDialog, I also pass data via props of DetailDialog. In the DetailDialog, I want pass data into Input text, so I must used state.data = props.data. (because can't change value of props. – Tùng Huynh May 24 '19 at 14:23
  • Besides, when I have many dialog, that need call in AdMenu, I must create state/openDialog func/closeDialog func for each dialog. Such as detailOpen, approveDialogOpen, ... State var. And open close dialog function. The code become confused – Tùng Huynh May 24 '19 at 14:29
  • Those are the most common approaches to manipulate a child component. Besides, more dialogs and additional data were not part of the question. – Rallen May 24 '19 at 14:35