A Better API for the Resize Observer

Home » A Better API for the Resize Observer

Resize Observer, Mutation Observer, and Intersection Observers are all good APIs that are more performant than their older counterparts:

The API for these three observers are quite similar (but they have their differences which we will go into later). To use an observer, you have to follow the steps below:

  1. Create a new observer with the new keyword: This observer takes in an observer function to execute.
  2. Do something with the observed changes: This is done via the observer function that is passed into the observer.
  3. Observe a specific element: By using the observe method.
  4. (Optionally) unobserve the element: By using the unobserve or disconnect method. (depending on which observer you’re using).

In practice, the above steps looks like this with the ResizeObserver.

// Step 1: Create a new observer
const observer = new ResizeObserver(observerFn)

// Step 2: Do something with the observed changes
function observerFn (entries) 
  for (let entry of entries) 
    // Do something with entry
  


// Step 3: Observe an element
const element = document.querySelector('#some-element')
observer.observe(element);

// Step 4 (optional): Disconnect the observer
observer.disconnect(element)

This looks clear (and understandable) after the steps have been made clear. But it can look like a mess without the comments:

const observer = new ResizeObserver(observerFn)

function observerFn (entries) 
  for (let entry of entries) 
    // Do something with entry
  


const element = document.querySelector('#some-element')
observer.observe(element);

The good news is: I think we can improve the observer APIs and make them easier to use.

The Resize Observer

Let’s start with the ResizeObserver since it’s the simplest of them all. We’ll begin by writing a function that encapsulates the resizeObserver that we create.

function resizeObserver () 
  // ... Do something

The easiest way to begin refactoring the ResizeObserver code is to put everything we’ve created into our resizeObserver first.

function resizeObserver () 
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) 
    for (let entry of entries) 
      // Do something with entry
    
  

  const node = document.querySelector('#some-element')
  observer.observe(node);

Next, we can pass the element into the function to make it simpler. When we do this, we can eliminate the document.querySelector line.

function resizeObserver (element) 
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) 
    for (let entry of entries) 
      // Do something with entry
    
  

  observer.observe(node);

This makes the function more versatile since we can now pass any element into it.

// Usage of the resizeObserver function
const node = document.querySelector('#some-element')
const obs = resizeObserver(node)

This is already much easier than writing all of the ResizeObserver code from scratch whenever you wish to use it.

Next, it’s quite obvious that we have to pass in an observer function to the callback. So, we can potentially do this:

// Not great
function resizeObserver (node, observerFn) 
  const observer = new ResizeObserver(observerFn)
  observer.observe(node);

Since observerFn is always the same — it loops through the entries and acts on every entry — we could keep the observerFn and pass in a callback to perform tasks when the element is resized.

// Better 
function resizeObserver (node, callback) 
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) 
    for (let entry of entries) 
      callback(entry)
    
  

  observer.observe(node);

To use this, we can pass callback into the resizeObserver — this makes resizeObserver operate somewhat like an event listener which we are already familiar with.

// Usage of the resizeObserver function
const node = document.querySelector('#some-element')
const obs = resizeObserver(node, entry => 
  // Do something with each entry
)

We can make the callback slightly better by providing both entry and entries. There’s no performance hit for passing an additional variable so there’s no harm providing more flexibility here.

function resizeObserver (element, callback) 
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) 
    for (let entry of entries) 
      callback( entry, entries )
    
  

  observer.observe(element);

Then we can grab entries in the callback if we need to.

// Usage of the resizeObserver function
// ...
const obs = resizeObserver(node, ( entry, entries ) => 
  // ...
)

Next, it makes sense to pass the callback as an option parameter instead of a variable. This will make resizeObserver more consistent with the mutationObserver and intersectionObserver functions that we will create in the next article.

function resizeObserver (element, options = ) 
  const  callback  = options
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) 
    for (let entry of entries) 
        callback( entry, entries )
      
  

  observer.observe(element);

Then we can use resizeObserver like this.

const obs = resizeObserver(node, 
  callback ( entry, entries ) 
    // Do something ...
  
)

The observer can take in an option too

ResizeObserver‘s observe method can take in an options object that contains one property, box. This determines whether the observer will observe changes to content-box, border-box or device-pixel-content-box.

So, we need to extract these options from the options object and pass them to observe.

function resizeObserver (element, options = ) 
  const  callback, ...opts  = options
  // ...
  observer.observe(element, opts);

Optional: Event listener pattern

I prefer using callback because it’s quite straightforward. But if you want to use a standard event listener pattern, we can do that, too. The trick here is to emit an event. We’ll call it resize-obs since resize is already taken.

function resizeObserver (element, options = ) 
  // ...
  function observerFn (entries) 
    for (let entry of entries) 
      if (callback) callback( entry, entries )
      else 
        node.dispatchEvent(
          new CustomEvent('resize-obs', 
            detail:  entry, entries ,
          ),
        )
      
    
  

  // ...

Then we can listen to the resize-obs event, like this:

const obs = resizeObserver(node)
node.addEventListener('resize-obs', event => 
  const  entry, entries  = event.detail
)

Again, this is optional.

Unobserving the element

One final step is to allow the user to stop observing the element(s) when observation is no longer required. To do this, we can return two of the observer methods:

  • unobserve: Stops observing one Element
  • disconnect: Stops observing all Elements
function resizeObserver (node, options = ) 
  // ...
  return 
    unobserve(node) 
      observer.unobserve(node)
    ,
    
    disconnect() 
      observer.disconnet()
    
  

Both methods do the same thing for what we have built so far since we only allowed resizeObserver to observe one element. So, pick whatever method you prefer to stop observing the element.

const obs = resizeObserver(node, 
  callback ( entry, entries ) 
    // Do something ...
  
)

// Stops observing all elements 
obs.disconect()

With this, we’ve completed the creation of a better API for the ResizeObserver — the resizeObserver function.

Code snippet

Here’s the code we’ve wrote for resizeObserver

export function resizeObserver(node, options = ) 
  const observer = new ResizeObserver(observerFn)
  const  callback, ...opts  = options

  function observerFn(entries) 
    for (const entry of entries) 
      // Callback pattern
      if (callback) callback( entry, entries, observer )
      // Event listener pattern
      else 
        node.dispatchEvent(
          new CustomEvent('resize-obs', 
            detail:  entry, entries, observer ,
          )
        )
      
    
  
 
  observer.observe(node)

  return 
    unobserve(node) 
      observer.unobserve(node)
    ,
    
    disconnect() 
      observer.disconnect()
    
  

Using this in practice via Splendid Labz

Splendid Labz has a utils library that contains an enhanced version of the resizeObserver we made above. You can use it if you wanna use a enhanced observer, or if you don’t want to copy-paste the observer code into your projects.

import  resizeObserver  from '@splendidlabz/utils/dom'

const node = document.querySelector('.some-element')
const obs = resizeObserver(node, 
  callback ( entry, entries ) 
    /* Do what you want here */
  
)

Bonus: The Splendid Labz resizeObserver is capable of observing multiple elements at once. It can also unobserve multiple elements at once.

const items = document.querySelectorAll('.elements')
const obs = resizeObserver(items, 
  callback ( entry, entries ) 
    /* Do what you want here */
  
)

// Unobserves two items at once
const subset = [items[0], items[1]]
obs.unobserve(subset) 

Found this refactoring helpful?

Refactoring is ultra useful (and important) because its a process that lets us create code that’s easy to use or maintain.

If you found this refactoring exercise useful, you might just love how I teach JavaScript to budding developers in my Learn JavaScript course.

In this course, you’ll learn to build 20 real-world components. For each component, we start off simple. Then we add features and you’ll learn to refactor along the way.

That’s it!

Hope you enjoyed this piece and see you in the next one.


A Better API for the Resize Observer originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

​ 

Leave a Comment

Your email address will not be published. Required fields are marked *