[Deep Dive into the Observer API] 4. MutationObserver
[Deep Dive into the Observer API] 4. MutationObserver
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.
- The target DOM
- A callback method
- 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.