|$ curl https://forge-ai.dev/api/markdown?path=docs/web-platform/mutation-observer
$cat docs/mutation-observer.md
updated Recently·22 min read·published
Mutation Observer
Introduction
Mutation Observer watches for changes to the DOM tree — added/removed nodes, attribute changes, and text content modifications. It replaced the deprecated Mutation Events API and fires callbacks asynchronously in batches, making it far more performant.
Basic Usage
basic-observer.ts
TypeScript
| 1 | // Create a Mutation Observer |
| 2 | const observer = new MutationObserver((mutations) => { |
| 3 | for (const mutation of mutations) { |
| 4 | if (mutation.type === "childList") { |
| 5 | console.log("Nodes added:", mutation.addedNodes.length); |
| 6 | console.log("Nodes removed:", mutation.removedNodes.length); |
| 7 | } |
| 8 | if (mutation.type === "attributes") { |
| 9 | console.log("Attribute changed:", mutation.attributeName); |
| 10 | console.log("Old value:", mutation.oldValue); |
| 11 | } |
| 12 | if (mutation.type === "characterData") { |
| 13 | console.log("Text content changed:", mutation.target.textContent); |
| 14 | } |
| 15 | } |
| 16 | }); |
| 17 | |
| 18 | // Start observing |
| 19 | observer.observe(document.getElementById("app")!, { |
| 20 | childList: true, // Watch for added/removed nodes |
| 21 | attributes: true, // Watch for attribute changes |
| 22 | characterData: true, // Watch for text content changes |
| 23 | subtree: true, // Watch all descendants (not just direct children) |
| 24 | attributeOldValue: true, // Record previous attribute values |
| 25 | attributeFilter: ["class", "style"], // Only watch specific attributes |
| 26 | }); |
| 27 | |
| 28 | // Stop observing |
| 29 | observer.disconnect(); |
| 30 | |
| 31 | // Get pending mutations (not yet delivered) |
| 32 | const records = observer.takeRecords(); |
Practical Use Cases
use-cases.ts
TypeScript
| 1 | // Use Case 1: Auto-save form content |
| 2 | const form = document.querySelector("form")!; |
| 3 | const autoSaveObserver = new MutationObserver(() => { |
| 4 | const data = new FormData(form); |
| 5 | localStorage.setItem("draft", JSON.stringify(Object.fromEntries(data))); |
| 6 | }); |
| 7 | |
| 8 | autoSaveObserver.observe(form, { |
| 9 | childList: true, |
| 10 | subtree: true, |
| 11 | characterData: true, |
| 12 | attributes: true, |
| 13 | }); |
| 14 | |
| 15 | // Use Case 2: Detect dynamically added elements |
| 16 | const listObserver = new MutationObserver((mutations) => { |
| 17 | for (const mutation of mutations) { |
| 18 | for (const node of mutation.addedNodes) { |
| 19 | if (node instanceof HTMLElement && node.classList.contains("new-item")) { |
| 20 | initItem(node); // Initialize new items |
| 21 | } |
| 22 | } |
| 23 | } |
| 24 | }); |
| 25 | |
| 26 | listObserver.observe(document.getElementById("list")!, { |
| 27 | childList: true, |
| 28 | subtree: true, |
| 29 | }); |
| 30 | |
| 31 | // Use Case 3: React to class/style changes (e.g., theme switching) |
| 32 | const themeObserver = new MutationObserver((mutations) => { |
| 33 | for (const mutation of mutations) { |
| 34 | if (mutation.attributeName === "class") { |
| 35 | const isDark = document.documentElement.classList.contains("dark"); |
| 36 | updateThemeIcon(isDark); |
| 37 | } |
| 38 | } |
| 39 | }); |
| 40 | |
| 41 | themeObserver.observe(document.documentElement, { |
| 42 | attributes: true, |
| 43 | attributeFilter: ["class"], |
| 44 | }); |
ℹ
info
Always call observer.disconnect() when the observed element is removed (e.g., in React useEffect cleanup). Undisconnected observers leak memory.
React Patterns
react-hook.tsx
TypeScript
| 1 | // React hook for Mutation Observer |
| 2 | import { useEffect, useRef } from "react"; |
| 3 | |
| 4 | function useMutationObserver( |
| 5 | callback: (mutations: MutationRecord[]) => void, |
| 6 | options: MutationObserverInit = { childList: true, subtree: true } |
| 7 | ) { |
| 8 | const ref = useRef<Node>(null); |
| 9 | |
| 10 | useEffect(() => { |
| 11 | if (!ref.current) return; |
| 12 | const observer = new MutationObserver(callback); |
| 13 | observer.observe(ref.current, options); |
| 14 | return () => observer.disconnect(); |
| 15 | }, [callback, options]); |
| 16 | |
| 17 | return ref; |
| 18 | } |
| 19 | |
| 20 | // Usage |
| 21 | function AutoSaveEditor() { |
| 22 | const ref = useMutationObserver( |
| 23 | (mutations) => { |
| 24 | console.log("Content changed, auto-saving..."); |
| 25 | // Debounce save logic here |
| 26 | }, |
| 27 | { childList: true, subtree: true, characterData: true } |
| 28 | ); |
| 29 | |
| 30 | return <div ref={ref} contentEditable suppressContentEditableWarning />; |
| 31 | } |
$Blueprint — Engineering Documentation·Section ID: WP-MO-01·Revision: 1.0