Cleaning up Resources using MutationObserver

Cleaning up resources?

Let’s say you’ve written a shiny new component in your favorite framework and somewhere along the way you’ve allocated a resource that cannot be automatically cleaned up by the browser.

Maybe you attached an event handler to the resize event on the window.  Maybe you passed a callback to a global object.  Whatever the case, you need to tidy up those resources at some point.

Easy enough, right?  Put a dispose method on our object to clean up it’s leftovers and make sure it’s called before the object is discarded.

class MyD3Component {
  constructor(targetElement: HTMLElement) {
    //create some resources that need to be cleaned up
  }

  dispose() {
    //clean up my resources
  }
}

Problem solved?

Problem not quite solved

What if, for whatever reason, your component doesn’t have control over the parent?  You could trust that the user will do the right thing and call dispose for you but you can’t guarantee it.

As an alternative, can we automatically clean up our resources as soon as our containing DOM element is removed?

Yes.  Yes we can.

Using MutationObserver

The MutationObserver API (which has pretty great browser support) lets you listen to changes made to a DOM node and react to it.  We can use it here to perform our cleanup.

When we create an instance of MutationObserver we specify a callback that gets details of changes made to the parent.  If those changes include the removal of our target element then we can call dispose.

class MyD3Component {
  constructor(targetElement: HTMLElement) {
    //create some resources that need to be cleaned up

    const observer = new MutationObserver(mutations => {
      //get a flattened list of all removed elements
      const removedElements = mutations
        .map(m => m.removedNodes)
        .reduce((a, b) => a.concat(Array.prototype.slice.apply(b)), []);

      //dispose if my target element was removed
      if (removedElements.indexOf(targetElement) !== -1) this.dispose();
    });

    observer.observe(targetElement.parentNode, { childList: true });
  }

  dispose() {
    //clean up my resources
  }
}

Here we are observing the parent of our target node, not the node itself (which would not be notified if removed).  We need to specify { childList: true } as the second parameter to be notified of additions and removals of child items.

Disposing the Observer

Finally, we need to make sure that the observer itself doesn’t cause a memory leak!  The observer is connected to the parentElement which (we assume) will still be hanging around, so we need to make sure that we disconnect it as part of disposal.

With everything pulled together the final version looks like this…

class MyD3Component {
  private observer: MutationObserver;

  constructor(targetElement: HTMLElement) {
    //create some resources that need to be cleaned up

    this.observer = new MutationObserver(mutations => {
      //get a flattened list of all removed elements
      const removedElements = mutations
        .map(m => m.removedNodes)
        .reduce((a, b) => a.concat(Array.prototype.slice.apply(b)), []);

      //dispose if my target element was removed
      if (removedElements.indexOf(targetElement) !== -1) this.dispose();
    });

    this.observer.observe(targetElement.parentNode, { childList: true });
  }

  dispose() {
    //clean up my resources

    //clean up the observer
    this.observer && this.observer.disconnect();
    delete this.observer;
  }
}