Resize Observer, Mutation Observer, and Intersection Observers are all good APIs that are more performant than their older counterparts:
ResizeObserver
is better than theresize
eventMutationObserver
replaces the now deprecated Mutation EventsIntersectionObserver
lets you do certainscroll
interactions with less performance overhead.
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:
- Create a new observer with the
new
keyword: This observer takes in an observer function to execute. - Do something with the observed changes: This is done via the observer function that is passed into the observer.
- Observe a specific element: By using the
observe
method. - (Optionally) unobserve the element: By using the
unobserve
ordisconnect
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:
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.