blog.itcode.devblog.itcode.dev

[Deep Dive into the Observer API] 4. MutationObserver

The MutationObserver API is an observer specialized for detecting changes in the DOM. It can detect not only size changes like width or height, but also changes in attributes like class. Let's look at how to use this observer, and how to use it conveniently in React through a custom hook.

[Deep Dive into the Observer API] 4. MutationObserver

The MutationObserver API is an observer specialized for detecting changes in the DOM. It can detect not only size changes like width or height, but also changes in attributes like class. Let's look at how to use this observer, and how to use it conveniently in React through a custom hook.
RWB0104
@RWBwritten at 2024-06-23 17:18:32
Deep Dive into the Observer API

시리즈 모아보기

Deep Dive into the Observer API

2 / 4

MutationObserver is an observer that detects changes in the DOM. It can detect any DOM-related change, whatever it may be. For instance, this includes style attributes such as width or padding, as well as class, id, and the addition of child nodes.

Because it detects changes in the DOM, it can serve as a replacement for ResizeObserver, which detects DOM size changes.

TYPESCRIPT

const mo = new MutationObserver(callback);

mo.observe(tag, options);

You can pass options when registering an element to fine-tune the detailed settings. It has the MutationObserverInit type.

If you specify the desired attribute names as an array in attributeFilter, the observer will filter and only detect changes to attributes with those names.

TYPESCRIPT

{
	// ...
	attributeFilter: [ 'id', 'class' ]
}

If specified as above, the observer only detects changes when id or class changes. If no value is assigned and it is undefined, all changes are detected.

MutationObserver runs when the DOM changes. Depending on the situation, you may want to build logic based on the value before the change, and the attributeOldValue setting is useful for that.

TYPESCRIPT

{
	// ...
	attributeOldValue: true
}

If specified as above, when an attribute change is detected, the value before the change is also provided. null is returned if it is the first change (so there is no previous attribute to reference), or if the change is not an attribute change (such as the addition of a child node).

Specifies whether to detect changes to DOM attributes.

TYPESCRIPT

{
	// ...
	attributes: true
}

If you want to detect changes to DOM attributes such as id, class, or style, set this option to true.

If a value is specified for attributeFilter or attributeOldValue, this option can be omitted.

Specifies whether to detect changes to the DOM's text node (node.value).

TYPESCRIPT

{
	// ...
	characterData: true
}

If the DOM's textContent value changes, this detects it.

TYPESCRIPT

function handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
	const tag = document.getElementById('target');

	if (tag?.firstChild?.textContent) {
		tag.firstChild.textContent = e.currentTarget.value;
	}
}

If the tag.firstChild.textContent value changes as above, a characterData event is detected.

If set to true, similar to attributeOldValue, when the text node value changes, the value before the change is also returned.

TYPESCRIPT

{
	// ...
	characterDataOldValue: true
}

null is returned if it is the first change (so there is no previous attribute to reference), or if the change is not an attribute change (such as the addition of a child node).

Detects when a child node is added to or removed from the target DOM.

TYPESCRIPT

{
	// ...
	childList: true
}

With this kind of setting, you can actively detect structural changes in a specific DOM.

The subtree option lets you extend the scope of detection to descendant nodes as well.

TYPESCRIPT

{
	// ...
	subtree: true
}

If this option is enabled, it also detects attributes, characterData, and childList changes in descendant nodes. The detection criteria are the same as those assigned to the parent's options.

For example, if only attributes detection is enabled and subtree is applied, you can detect attribute changes across the element itself and all of its descendant nodes.

Through the callback method, you can specify logic to run when the DOM changes. It has the MutationRecord type. This can be expressed in code as follows.

TYPESCRIPT

const mo = new MutationObserver((record) => {});

If there are added nodes, addedNodes returns them as an array. It is returned when the childList option, which is related to node addition/removal, is enabled.

HTML

<!-- Initial value -->
<div id="target">
	
</div>

<!-- addedNodes returned -->
<div id="target">
	<p>just added!</p>
</div>

In the example above, the information for the p node added under #target is included.

If there are removed nodes, removedNodes returns them as an array. The condition is the same as for addedNodes.

HTML

<!-- Initial value -->
<div id="target">
	<p>just added!</p>
</div>

<!-- removedNodes returned -->
<div id="target">
	
</div>

If an attribute changed, attributeName returns the name of the changed attribute. It is returned when the attributes option, which is related to attribute changes, is enabled.

HTML

<!-- Initial value -->
<div id="target">
	
</div>

<!-- attributeName returned -->
<div id="target" class="hi">
	
</div>

In the example above, the changed attribute name class, which was added to #target, is returned.

If the attribute namespace changed, attributeNamespace returns the changed attribute namespace. Likewise, it is returned when the attributes option is enabled.

You can understand it as: if the changed attribute has a namespace, that namespace is returned along with it.

HTML

<!-- Initial value -->
<div id="target">
	
</div>

<!-- attributeName returned -->
<div id="target" class="hi">
	
</div>

If, when creating class, it is created bundled under a namespace called ns via setAttributeNS, the code would look like below.

TYPESCRIPT

tag.setAttributeNS("ns", "class", "hi");

In an example like the above, attributeName returns class, and attributeNamespace returns ns.

Returns the DOM sibling that follows the added or removed node. It is returned when the childList option, which is related to node addition/removal, is enabled.

Returns the DOM sibling that precedes the added or removed node.

If a change event is detected, returns the value before the change. It only returns a value when the attributeOldValue and characterDataOldValue options are true.

Returns the tag on which the event occurred.

Returns the type of the event that occurred. It returns the name of the event that occurred, from among the following values.

  • attributes
  • characterData
  • childList

Returns the MutationObserver object. Through this, MutationObserver can also be handled in a chained manner within the callback method.

Let's use MutationObserver conveniently through a custom hook.

TYPESCRIPT

export function useMutationObserver(): void
{
  //
}

Define the useMutationObserver method as above.

To use useMutationObserver, the following three elements are needed.

  1. The target DOM
  2. A callback method
  3. Options

Let's define these three elements as parameters. For the first one, an HTMLElement is normally required, but this hook will be built to accept not only the HTMLElement type but also a string, so that a tag can be targeted using a selector like #id or .class.

TYPESCRIPT

import { useEffect } from "react";

export type UseMutationObserverCallback = (entry: MutationRecord) => void;

/**
 * Hook method for applying MutationObserver
 *
 * @param {Element | string | null} ref: Element
 * @param {UseMutationObserverCallback} callback: Callback method
 * @param {MutationObserverInit} options: Options
 */
export function useMutationObserver(ref: Element | string | null, callback: UseMutationObserverCallback, options: MutationObserverInit): void
{
	useEffect(() =>
	{
		const ro = new MutationObserver((entries) =>
		{
			entries.forEach(callback);
		});
	}, [ ref, callback, options ]);
}

Initialize MutationObserver and assign it to the mo variable.

Next, assign the DOM specified by ref.

ref branches according to three possible states, as follows.

  • If it is null -> skip it.
  • If it is a string -> select the tag with the document.querySelector method, then register that tag.
  • If it is an HTMLElement -> since it is already an Element, register it immediately.

This was implemented by checking whether ref is a string via the typeof ref === "string" expression.

You can register a tag through the mo.observe() method.

TYPESCRIPT

import { useEffect } from "react";

export type UseMutationObserverCallback = (entry: MutationRecord) => void;

/**
 * Hook method for applying MutationObserver
 *
 * @param {Element | string | null} ref: Element
 * @param {UseMutationObserverCallback} callback: Callback method
 * @param {MutationObserverInit} options: Options
 */
export function useMutationObserver(ref: Element | string | null, callback: UseMutationObserverCallback, options: MutationObserverInit): void
{
	useEffect(() =>
	{
		const mo = new MutationObserver((entries) =>
		{
			entries.forEach(callback);
		});

		// If the DOM is valid
		if (ref)
		{
			// If ref is a string
			if (typeof ref === 'string')
			{
				const tag = document.querySelector(ref);

				// If the tag is valid
				if (tag)
				{
					mo.observe(tag, options);
				}
			}

			// If it is a DOM element
			else
			{
				mo.observe(ref, options);
			}
		}
	}, [ ref, callback, options ]);
}

Finally, add cleanup code so that MutationObserver registrations don't stack up every time the component re-renders.

MutationObserver can be removed via the mo.disconnect() method.

TYPESCRIPT

import { useEffect } from "react";

export type UseMutationObserverCallback = (entry: MutationRecord) => void;

/**
 * Hook method for applying MutationObserver
 *
 * @param {Element | string | null} ref: Element
 * @param {UseMutationObserverCallback} callback: Callback method
 * @param {MutationObserverInit} options: Options
 */
export function useMutationObserver(ref: Element | string | null, callback: UseMutationObserverCallback, options: MutationObserverInit): void
{
	useEffect(() =>
	{
		const mo = new MutationObserver((entries) =>
		{
			entries.forEach(callback);
		});

		// If the DOM is valid
		if (ref)
		{
			// If ref is a string
			if (typeof ref === 'string')
			{
				const tag = document.querySelector(ref);

				// If the tag is valid
				if (tag)
				{
					mo.observe(tag, options);
				}
			}

			// If it is a DOM element
			else
			{
				mo.observe(ref, options);
			}
		}

		return () =>
		{
			mo.disconnect();
		};
	}, [ ref, callback, options ]);
}

The full code is as above.

A simple example has been implemented in CodeSandbox.

By making use of MutationObserver like this, you can actively detect changes in the DOM.

Occasionally, when an element changes in a React component, it can become necessary to run additional code in another related component. To handle this, unrelated logic from other components sometimes gets inserted into a component that shouldn't need it.

Using MutationObserver, you can separate this out and implement it more simply.

The remaining observers are relatively less commonly used. Among them, we'll cover PerformanceObserver, which is related to performance.

# JavaScript# TypeScript# React# Observer API# MutationObserver
ship
blog.itcode.dev

Notes from the π-th Alpaca

7.0.1
Developed by RWB since 2021.057th upgraded at 2026.08