1

Our application consists of lots of legacy code written using jQuery and some newer React components. We have mostly been able to handle separating concerns between the two, most of our code is one or the other - but occasionally we have React Components within JQuery Components.

In most cases: JQuery creates a Div, the Div Element is passed to a ReactDOM.render and control for that div is controlled by react. Then before JQuery destroys the Div it will call ReactDOM.unmountComponentAtNode to do the tear down.

However we have some JQuery code that is being removed in a way that is not trivial to detect that some of it is actually React, creating a component memory leak.

The correct fix for us it to solve the tear down, so that the unMounts are always called correctly, but there are engineering risks in this as the legacy code is not the cleanest and the risk of introduction of new bugs is high.

My question is whether there is a way that React Components can spot that they have become orphaned. ComponentWillUnmount is not the answer (as it does not get called in this scenario).

Mark Fee
  • 122
  • 1
  • 11
  • *"...creating a component memory leak."* What makes you think that? I susect it's true that `componentWillUnmount` won't get called, but what is the component doing that might make that a memory leak? – T.J. Crowder May 15 '20 at 10:54
  • 1
    Maybe the wrong term to use, but the Component Exists in memory and is orphaned but is still there. These components subscribe to a service onMount and I noticed that they weren't unsubscribing (called in componentWillUnmount) so the service was still notifying and the list of subscriptions was growing. – Mark Fee May 15 '20 at 11:28
  • They may or may not actually still be in memory (it'll depend on what subscribing looks like), but yeah, if they need to unsubscribe from something... :-) – T.J. Crowder May 15 '20 at 11:30

1 Answers1

2

You could use a mutation observer on the component's parent, but I'd be surprised if it were really necessary.

Basically, you'd:

  • Use a ref on the top-level element returned by your component in render.
  • In componentDidMount, use current from the ref to find the parentNode and set up a mutation observer on it, watching for childList changes.
  • In the observer callback, look for a mutation record where the ref's current node in the list of removed nodes.
    • If so, do any cleanup that you have to do.

Live Example:

class Example extends React.Component {
    constructor(props) {
        super(props);
        this.ref = React.createRef(null);
        this.cleanedUp = false;
    }
    
    componentDidMount() {
        this.cleanedUp = false;
        // assert(this.ref.current !== null);
        if (this.observer) {
            this.observer.disconnect();
        }
        this.observer = new MutationObserver(records => {
            if (this.cleanedUp) {
                return;
            }
            for (const record of records) {
                for (const node of record.removedNodes) {
                    if (node === this.ref.current) {
                        this.cleanup();
                        return;
                    }
                }
            }
        });
        this.observer.observe(this.ref.current.parentElement, {
            childList: true,
            subtree: true
        });
    }
    
    componentWillUnmount() {
        this.cleanup();
    }

    cleanup() {
        console.log("Clean up here");
        this.observer.disconnect();
        this.observer = null;
        this.cleanedUp = true;
    }
    
    render() {
        return <div ref={this.ref}>This is the component's top level element</div>;
    }
}

const root = document.getElementById("root");
ReactDOM.render(<Example/>, root);

// Outside React...
setTimeout(() => {
    root.innerHTML = ""; // Bludgeon its contents
}, 1000);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

You might have to watch from further up in the document tree than just the component's immediate parent, but that's the general idea...


Note: Using for-of on the removedNodes as I have above relies on the browser making NodeList iterable. Modern browsers do, but slightly older ones may not. My answer here talks about how to make them iterable (assuming the iteration protocol is supported by the browser's JavaScript engine), or you might use forEach instead (also discussed there).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875