Why Observer APIs All Follow the Same Pattern
Modern browser APIs like ResizeObserver, MutationObserver, and IntersectionObserver consistently outperform their predecessors — resize events, deprecated Mutation Events, and scroll-based intersection checks, respectively. They share a similar four-step usage model:
- Instantiate the observer with a callback function via
new. - Handle changes inside that callback.
- Start observing an element with
observe(). - Optionally stop with
unobserve()ordisconnect().
Here’s how that plays out with 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)
With comments, the flow is understandable. Without them, it becomes harder to follow:
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: these APIs can be made significantly easier to work with through wrapping and refactoring. Starting with ResizeObserver — the simplest of the three — we can build a clean, reusable function.
Building a Wrapper Function
Begin by moving all observer logic inside a function:
function resizeObserver () {
// ... Do something
}
Then pass the target element as a parameter, which removes the document.querySelector step entirely:
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);
}
The function is now versatile — it accepts any element:
function resizeObserver (element) {
const observer = new ResizeObserver(observerFn)
function observerFn (entries) {
for (let entry of entries) {
// Do something with entry
}
}
observer.observe(node);
}
This alone removes the boilerplate of constructing an observer from scratch:
// Usage of the resizeObserver function
const node = document.querySelector('#some-element')
const obs = resizeObserver(node)
Handling the Callback More Cleanly
At this point, the observer function that loops through entries is duplicated across every call site. We can abstract that inner loop and accept a user-supplied callback instead, making the wrapper behave similarly to an event listener:
// Not great
function resizeObserver (node, observerFn) {
const observer = new ResizeObserver(observerFn)
observer.observe(node);
}
Usage becomes much lighter:
// Better
function resizeObserver (node, callback) {
const observer = new ResizeObserver(observerFn)
function observerFn (entries) {
for (let entry of entries) {
callback(entry)
}
}
observer.observe(node);
}
To maximize flexibility with no performance cost, the callback can receive both entry and the full entries array:
// Usage of the resizeObserver function
const node = document.querySelector('#some-element')
const obs = resizeObserver(node, entry => {
// Do something with each entry
})
You can then grab entries in the callback when needed:
function resizeObserver (element, callback) {
const observer = new ResizeObserver(observerFn)
function observerFn (entries) {
for (let entry of entries) {
callback({ entry, entries })
}
}
observer.observe(element);
}
Options and Configuration
ResizeObserver’s observe method accepts an options object with one property: box, which determines whether to track content-box, border-box, or device-pixel-content-box. The options should be passed through to observe:
function resizeObserver (element, options = {}) {
const { callback, ...opts } = options
// ...
observer.observe(element, opts);
}
One key design choice is accepting the callback as part of the options object rather than as a separate argument. This keeps the wrapper compatible with the mutationObserver and intersectionObserver counterparts we’ll build later:
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);
}
Usage then looks like this:
const obs = resizeObserver(node, {
callback ({ entry, entries }) {
// Do something ...
}
})
Stopping Observation
Any good wrapper must let you tear down the observer. ResizeObserver provides two methods:
unobserve: Stop observing a single element.disconnect: Stop observing all elements.
Since the wrapper only observes one element, both methods are functionally equivalent for our purposes — pick whichever fits your preference:
function resizeObserver (node, options = {}) {
// ...
return {
unobserve(node) {
observer.unobserve(node)
},
disconnect() {
observer.disconnet()
}
}
}
Putting it together, the final wrapper offers a complete, cleaner API:
const obs = resizeObserver(node, {
callback ({ entry, entries }) {
// Do something ...
}
})
// Stops observing all elements
obs.disconect()
Here’s the full code:
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()
}
}
}
Going Further
The Splendid Labz utils library contains an enhanced version of this same resizeObserver wrapper. It goes a step further by observing and unobserveing multiple elements in a single call:
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) 


