logo hsb.horse
← Back to snippets index

Snippets

Idempotent DOM Observer Setup via dataset

A simple pattern using data attributes to prevent duplicate MutationObserver registrations. No global registry needed, highly portable.

Published: Updated:

When MutationObserver is set up multiple times, duplicate observers can be registered on the same element. While managing them with a global Set or Map is an option, writing an observed flag into dataset is lightweight and easy to port.

Code

const OBSERVE_ID_KEY = 'observeId'
const isElementObserved = <T extends HTMLElement = HTMLElement>(
ele: T
) => Object.hasOwn(ele.dataset, OBSERVE_ID_KEY)
const setObserverId =
(id: string) =>
<T extends HTMLElement = HTMLElement>(ele: T): T => {
ele.dataset[OBSERVE_ID_KEY] = id
return ele
}
export const observeElement = (
observeId: string,
element: HTMLElement | string,
observerCallback: MutationCallback,
options: MutationObserverInit = { childList: true }
) => {
const observedElement =
element instanceof HTMLElement
? element
: document.querySelector<HTMLElement>(element)
if (observedElement && !isElementObserved(observedElement)) {
const observer = new MutationObserver(observerCallback)
observer.observe(observedElement, options)
setObserverId(observeId)(observedElement)
return observer
}
}

Usage

// Accepts both a selector string or an HTMLElement
observeElement(
'my-list-observer',
'#item-list',
(mutations) => {
for (const mutation of mutations) {
console.log('Change detected:', mutation.type)
}
}
)
// Calling again on the same element does not register a duplicate observer
observeElement('my-list-observer', '#item-list', callback)

How It Works

  1. Object.hasOwn(ele.dataset, OBSERVE_ID_KEY) checks for the presence of the data-observe-id attribute
  2. Only if unregistered, a MutationObserver is created and observe() is called
  3. After observation starts, the flag is written via ele.dataset[OBSERVE_ID_KEY] = id
  4. On subsequent calls, isElementObserved returns true and nothing happens

Because dataset values are written directly to the HTML data-* attribute, they are visible in DevTools.

Benefits

  • No global registry: State is stored on the element itself, eliminating the need to manage a separate Map or Set
  • Highly portable: No framework dependencies; works in any environment
  • Easy to debug: Attribute values can be inspected directly in DevTools

Caveats

When an element is removed from the DOM, its dataset information is also lost. If the same element is re-inserted, an observer will be registered again. Also, to stop an observer, you must call observer.disconnect() separately.