Discovering Observer Web APIs

Discovering Observer Web APIs

·

7 min read

When running in a browser environment, you can use many Web APIs in your JavaScript code. From simple stuff like accessing the DOM, through integrated payments, vibrations, cryptography, WebGL, canvas, all the way to infinity & beyond.

Today, we’ll be exploring a small section of Web APIs, something that one could call “observer-based Web APIs”. These are:

  • Mutation Observer API
  • Resize Observer API
  • Intersection Observer API

So, seemingly unrelated Web APIs, with different use-cases, but still, having one thing in common - the observer-based architecture. This means that they’ll share similar API structures, and in general, allow you to observe and react to certain changes and values.

With this little introduction, let’s jump right into it.

Mutation Observer API

Starting with arguably the oldest and most well-supported of the bunch (it even works on IE 11), we’ve got the Mutation Observer API.

Consisting solely of MutationObserver interface, it allows you to watch for changes, aka mutations to the DOM. You can watch for changes like an addition/removal of child nodes, alternation of character data, and augmentations of attributes. All that for only the target element or its entire subtree.

How does it work?

Usage of the MutationObserver is fairly simple. Just initiate it with a callback function and then use the observe() and disconnect() methods of the created instance, to respectively watch a DOM node for changes and stop the entire instance from any active watches.

// Example target element.
const target = document.getElementById("target");
/* Callback recieving the observer instance, 
   and a slew of mutations' info about a change that triggered it.
*/
const mutationObserver = new MutationObserver((mutations, observer) => {
  for (const mutation of mutations) {
    if (mutation.type === "childList") {
      // Child tree change.
      mutation.addedNodes; // Added DOM nodes.
      mutation.removedNodes; // Removed nodes.
      mutation.target; // The parent (relevant when watching the whole subtree)
    } else if (mutation.type === "attributes") {
      // Attribute change.
      mutation.attributeName; // Name of the changed attribute.
      mutation.oldValue; // Previous value of the attribute (if enabled in options)
    }
    /* For more mutation info see:
       https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
    */
  }
});

// Observe the target DOM node for the selected changes.
mutationObserver.observe(target, {
  /* For more options see: 
     https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit
  */
  attributes: true,
  childList: true,
  subtree: true,
});
// When no further observing is required.
mutationObserver.disconnect();

Apart from the two mentioned methods, there’s also the takeRecords() method, which returns an array of MutationRecords (the same one from the callback function) that were detected but not yet processed through the callback. It’s useful for doing final processing before disconnect().

// ...
const mutations = resizeObserverr.takeRecords();

mutationObserver.disconnect();

if (mutations) {
  // Run one, final callback.
  callback(mutations);
}

How is it useful?

MutationObserver can be used in many different ways. Especially when running as a 3rd-party script on a foreign website, it allows you to react to DOM changes as they happen.

On the other hand, when you’re creating a website from the ground up and have the knowledge and control over the DOM changes, MutationObserver can still be useful for watching when, e.g., when a 3rd-party widget or a specific part of it gets loaded.

MutationObserver is much faster, cleaner, and easier to use than running change-checking intervals. Still, observing nodes such as whole <body> with its entire subtree will definitely cause performance issues.

With this little overview of MutationObserver, you should have a pretty good understanding of how these observer-based Web APIs work and look like, as, like I’ve said, most of them have a similar structure.

Having said that, let’s explore another API!

Resize Observer API

Resize Observer API consists, like the previous API, of a single ResizeObserver interface, an instance of which has 3 methods - the base observe() and disconnect(), but also unobserve().

Now, Resize Observer API allows you to observe for resize changes of any DOM element. This means that you can not only watch for window resizes more efficiently than with the window.onresize event, or more accurately than with @media rules, but also that you can actually react to changes on an element-basis when, e.g., the user uses the resize handle, or layout update happens.

This is a very useful feature for many applications. You’ll no longer have to set intervals and use the costly getBoundingClientRect() method just to react to an element’s size change. However, it is pretty new and available only on newer versions of the evergreen browsers.

As for how you can use it:

// ...
/* Callback recieving the observer instance, 
   and a slew of resize entries for observed elements.
*/
const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    // The resized element's DOMRect - contains size and position info.
    entry.contentRect;
    entry.contentRect.width;
    entry.contentRect.height;
    entry.contentRect.x;
    entry.contentRect.y;
    /* For more resize entry info see:
       https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry
    */
  }
});

resizeObserver.observe(target);
// When no further observing is required
resizeObserver.disconnect();

Now, ResizeObserver in comparison to MutationObserver doesn’t have the takeRecords() method, and so it doesn’t queue the incoming changes.

To compensate for that, there’s an unobserve() method. It’s similar to disconnect() but instead of clearing the whole observer instance, it only “unobserves” the provided element, allowing the same observer to easier manages multiple elements.

// ...

observer.unobserve(target);

Intersection Observer API

Last but not least, we’ve got the Intersection Observer API. It can be used to observe intersections between parent and child elements (usually between any DOM element and root viewport). That’s useful for detecting, e.g., approximate scroll position (by placing a dummy element at the scroll position of interest), whether the user has displayed an ad or other widget, whether we should load more content in an infinite scroller, etc.

Browser support-wise, it sits between the previous 2 APIs, being supported by older versions of evergreen browsers than ResizeObserver, but still not by IE, like MutationObserver is.

Now, as for the actual API, it feels like a combination of the 2 prior ones. You’ve got a single interface - IntersectionObserver, an instance of which has all 4 previously-introduced methods, including both takeRecords() and unobserve(), all of which serve a similar purpose to their counterparts in both ResizeObserver and MutationObserver.

Just like with MutationObserver, IntersectionObserver also accepts an options object, but this time directly in the constructor call, right after the callback. All of the set options, or their defaults, can be later accessed directly through the observer’s instance.

// ...
/* Callback receiving the observer instance,
   and a slew of intersection entries for observed elements.
*/
const intersectionObserver = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      entry.isIntersecting; // If the element is intersecting with the root.
      entry.intersectionRatio; // Ratio of intersection.

      /* For more intersection entry info see:
        https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
      */
    }
  },
  {
    /* For options see:
       https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver
    */
  }
);
intersectionObserver.observe(target);
// To unobserve given element.
intersectionObserver.unobserve(target);
// Take last entries before disconnecting.
const entries = intersectionObserver.takeRecords();
// Disconnect observer completely.
intersectionObserver.disconnect();

There’s more

Now, apart from the DOM-related Observers we’ve just covered, there’s also the PerformanceObserver (Performance Observer API) and ReportingObserver (Reporting Observer API) - both having APIs similar to MutationObserver (observe(), disconnect(), takeRecords()) and can be used to observe performance measurements, and reports respectively.

Both of these observers are, in general, less used than the 3 listed and ReportingObserver is even a part of the Reporting API, which’s experimental and Chromium-exclusive right now.

With that said, I hope this post has given you a better understanding and possibly even reassurance to try out and use at least one of the Web API observers in your projects. They’re all uniquely useful and can provide you with clean interfaces to functionalities that were either hard or impossible to achieve in the past.

For more web development content, covering frameworks, Web APIs, and more, follow me on Twitter, Facebook, or through my newsletter. Thanks for reading, and happy coding!

This post was written with ease, made grammatically correct, and cross-posted here within 1 click thanks to CodeWrite with its great editor, smooth Grammarly integration, and "one-click publishing". Try it for free, and use the code first100 to get 20% off your subscription (only $2.40/month, or $24/year!)