MutationObserver lets you react to DOM changes efficiently. It replaced the deprecated Mutation Events API.
Basic Usage
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log("Type:", mutation.type);
console.log("Target:", mutation.target);
if (mutation.type === "childList") {
console.log("Added:", mutation.addedNodes);
console.log("Removed:", mutation.removedNodes);
}
if (mutation.type === "attributes") {
console.log("Attribute:", mutation.attributeName);
console.log("Old value:", mutation.oldValue);
}
});
});
observer.observe(document.getElementById("app"), {
childList: true, // Watch for added/removed children
attributes: true, // Watch for attribute changes
characterData: true, // Watch for text content changes
subtree: true, // Watch entire subtree
attributeOldValue: true, // Record old attribute values
});Practical Use Cases
1. Auto-Initialize Components
// Initialize tooltips on dynamically added elements
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const tooltips = node.querySelectorAll("[data-tooltip]");
tooltips.forEach(initTooltip);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });2. Content Change Detection
// Detect when a third-party script modifies your DOM
function watchForTampering(element) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
mutation.addedNodes.forEach((node) => {
if (node.tagName === "SCRIPT" || node.tagName === "IFRAME") {
console.warn("Suspicious element added:", node);
node.remove();
}
});
}
});
});
observer.observe(element, { childList: true, subtree: true });
}3. Resize Detection (Before ResizeObserver)
// Watch for size changes via style attribute
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "style") {
handleResize(mutation.target);
}
});
});
observer.observe(element, { attributes: true, attributeFilter: ["style"] });Performance Tips
- Use
attributeFilterto limit which attributes are watched - Disconnect when no longer needed:
observer.disconnect() - Mutations are batched and delivered asynchronously (microtask)
- Avoid triggering DOM changes inside the callback (causes infinite loops)
React Integration
function useMutationObserver(ref, callback, options) {
useEffect(() => {
if (!ref.current) return;
const observer = new MutationObserver(callback);
observer.observe(ref.current, options);
return () => observer.disconnect();
}, [ref, callback, options]);
}