JavaScript Observers and You

Native observers empower your JavaScript to subscribe to events like the alteration of the DOM, an elements position in relation to the viewport and even the resizing of individual elements. Each observer follows a similar pattern to construct and they offload complicated and sometimes tightly coupled functionality to the browser.

Still not convinced? Let’s walk through three of the most powerful JavaScript observers and observe ourselves how they’ll make our lives easier. To start, let’s check out our mutation observer.

MutationObserver

Ever needed to know when an element is altered or if a node is being inserted? Besides having the best name ever, a mutation observer allows you to observe changes to the DOM. Here’s an example use case:

Let’s say we have a class added to an element asynchronously, and there is a separate function that relies on this class being present. During our development, we notice that sometimes our function fires before the class is added due to racing conditions. Oof, incoming headache, right? Not so fast, there’s an observer for that.

First, we need to define our observer. Our observer is a MutationObserver object which will fire a callback function whenever our observation conditions are met:

const observer = new MutationObserver(mutationCallback);

Next, we need to tell our observer to start watching. We do this through its observe() method:

const el = document.querySelector(.yourElement’);

observer.observe(el, { attributes: true });

Here, we tell it to watch .yourElement and, in the configuration object, to also watch its attributes, like class. Whenever an attribute is altered on .yourElement, our callback function will run.

Other configuration options are available, but some notable ones include childList and subtree. childList knows when child elements are added or removed and subtree is something I’ve never used, but you can set it to true if you want to watch the parent node.

Next, we define our callback function, which we named mutationCallback above.

function mutationCallback(mutations, observer) {
if (mutations[0].target.classList.contains('added')) {
hiddenMsg.classList.remove('hidden');
observer.disconnect();
}
}

If our mutations target gets the added class, then remove the hidden class from our hiddenMsg element and disconnect the observer. Disconnecting the observer tells it to stop watching for changes and is generally a best practice if you don’t need to continually watch the element for changes.

Here, we’re also only targeting the first mutation with mutations[0]. If you’re unsure that this element might have multiple mutations, you’ll need to create a forEach and loop through each one to check our condition.

I created a codepen to where you can see all of this in action.

Pretty cool, right? If we can watch the DOM for changes, what else can we watch? How about the viewport?

IntersectionObserver

Ever have to custom roll a lazy-loaded image loader - writing flaky and expensive calculations based off an element’s offset? The intersection observer allows you to watch when an element intersects with the viewport, and I hope you’re already excited by what this implies. IntersectionObserver is here to deliver you to the promised land.

Just like our MutationObserver, an intersection observer follows the same pattern to set up: we need to define our observer and a callback function to fire when our conditions are met:

const observer = new IntersectionObserver(intersectionCallback);

Next, like before, we need to tell our IntersectionObserver to start watching with the observe method.

observer.observe(document.querySelector(.yourElement’));

Finally our callback, which we called intersectionCallback above. I created a codepen to simulate lazy-loading here.

function intersectionCallback(imgContainers) {
imgContainers.forEach(imgContainer => {
const img = imgContainer.target.querySelector('.img');
if (imgContainer.intersectionRatio > 0) {
img.classList.add('visible');
}
});
}

In this callback, we say “Hey - intersectionObserver, every time one of our image containers hits the viewport (imgContainer.intersectionRatio > 0), load the image (img.classList.add('visible');)”. And that’s it - no more fragile viewport calculations. As a front-end developer, there’s nothing I love more than offloading work to the browser.

While lazy loading images is an immediate choice, it can be useful in all sorts of applications like flyouts and triggered animations.

ResizeObserver

Rolling in at number three, and with the least browser support so far (sorry non-Chrome users), but a really cool feature to learn nonetheless, we have the ResizeObserver.

In 2019, we’re blessed to have media queries make our lives easier - it’s kinda how this whole responsive web works. But what if instead of wanting to know when the viewport changed in width, we wanted to take action when an element’s width changed? Hi there, ResizeObserver.

Like our previous two examples, our observer follows the same three steps: create our observer with a callback function; tell the observer to start watching an element; decide what our callback function will do when our conditions are met. Let’s check out each step.

Here’s our resize observer:

const observer = new ResizeObserver(resizeCallback);

Here’s us telling the code to observe each one of our boxes with a forEach.

boxes.forEach(box => {
observer.observe(box);
});

And finally, here’s our callback function that tells the code what to do when our conditions are met:

function resizeCallback(boxes) {
boxes.forEach(box => {
if (box.contentRect.width > 400) {
box.target.classList.remove('thin-border');
box.target.classList.add('thick-border');
} else {
box.target.classList.remove('thick-border');
box.target.classList.add('thin-border');
}
});
}

View this on Codepen

We’re saying that whenever one of our boxes widths is greater than 400, set the class to thick-border, which has a border-width of 5vw, otherwise set it to thin-border, which has a border-width of 1vw. Why would we need this? Good question - maybe design wanted a mighty thick border around our images but wanted them smaller whenever an image reached a certain width. We could define this with a media query and CSS, but that only affects change on viewport width, not an element’s width, so the resizing would leave large breakpoint gaps in between.

Observers - who knew they were so awesome? Well, maybe you did already, but hopefully you learned something new today. There’s a whole lot more observers out there, so explore the docs to your heart’s content, or if you’d like to see a part two to this post where I go over more, let me know, and keep an eye out.