Using MutationObserver to watch for DOM Changes

Using a MutationObserver lets you watch for and react to changes to a page element. For example, imagine that you have a list where users can add, remove, or reorder items. Each time the list changes, you need to take some action, such as making an AJAX call to save their changes.

You might do something like this:

function onListUpdate() {
  // The functionality you want to run every time
  // there's an update.
}

function addNewListItem() {
  // Create the new list item.
  onListUpdate(); 
}

function removeListItem() {
  // Remove a list item.
  onListUpdate();
}

function reorderListItem() {
  // Move a list item to a different position in the list.
  onListUpdate();
}

As you can see, this requires you carefully chain functions so that you call onListUpdate() at the end of every update.

Enter MutationObserver. Once created, a MutationObserver watches a particular DOM node for the changes you specify: changes to attributes, changes to the character data (e.g. the element’s text content), and/or elements being added or removed within that node.

function onListUpdate() {
  // The functionality you want to run every time
  // there's an update.
}

function addNewListItem() {
  // Create the new list item.
  // Notice that we're no longer calling onListUpdate()
  // at the end of every function.
}

function removeListItem() {
  // Remove a list item.
}

function reorderListItem() {
  // Move a list item to a different position in the list.
}

// Create a new observer, passing the callback function 
// to run on mutation.
const observer = new MutationObserver(onListUpdate);
// Find the list we want to observe.
const targetList = document.getElementById('the-list');
// Start watching for changes to the list.
// We're watching only for changes to direct children,
// i.e. new or removed list items.
observer.observe(targetList, { childList: true });

The callback function for a MutationObserver accepts two parameters, an array of MutationRecords and a reference to the observer that triggered the callback. This allows you to watch for different types of changes and respond accordingly.

const observer = new MutationObserver((records, observer) => {
  records.forEach(record => {
    if (record.type === 'attributes') {
      onAttributeChange();
    } else if (record.type === 'childList') {
      const parentNode = record.target;
      if (record.addedNodes) {
        onAddedNodes(parentNode);
      }
    }
  });
});

Finally, to stop watching for changes, run observer.disconnect().