Skip to main content

Command Palette

Search for a command to run...

Intersection and Mutation observers explained

Updated
7 min read
Intersection and Mutation observers explained
T

Just a guy who loves to write code and watch anime.

Understanding Web Observers

The browser offers several observer APIs that let you monitor and react to specific changes in the DOM. Two of the most useful are Intersection Observer and Mutation Observer. They solve different problems and are used in different scenarios.

Intersection Observer Basics

Intersection Observer detects when an element enters or exits the viewport or intersects with another element. Before this API, developers had to use scroll event listeners with getBoundingClientRect(), which caused performance issues.

How Intersection Observer Works

The Intersection Observer API works by creating an observer that watches a target element and fires a callback when that element intersects with a specified element (usually the viewport).

const observer = new IntersectionObserver(callback, options);
observer.observe(targetElement);

The callback receives an array of IntersectionObserverEntry objects, which contain information about the intersection.

Key Options for Intersection Observer

const options = {
  root: null, // The element to use as viewport (null = browser viewport)
  rootMargin: "0px", // Margin around the root
  threshold: 0.5, // Percentage of target visibility to trigger callback (0-1)
};

Using Intersection Observer in React and TypeScript

Here's how to implement an Intersection Observer in a React component with TypeScript:

import React, { useEffect, useRef, useState } from "react";

interface IntersectionProps {
  threshold?: number;
  rootMargin?: string;
  onIntersect?: () => void;
  children: React.ReactNode;
}

const IntersectionComponent: React.FC<IntersectionProps> = ({
  threshold = 0,
  rootMargin = "0px",
  onIntersect,
  children,
}) => {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const currentRef = ref.current;
    if (!currentRef) return;

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setIsVisible(true);
            if (onIntersect) onIntersect();
          } else {
            setIsVisible(false);
          }
        });
      },
      { threshold, rootMargin }
    );

    observer.observe(currentRef);

    return () => {
      if (currentRef) {
        observer.unobserve(currentRef);
      }
    };
  }, [threshold, rootMargin, onIntersect]);

  return (
    <div ref={ref}>
      {children}
      {isVisible && <div>Element is visible!</div>}
    </div>
  );
};

Creating a Reusable Intersection Observer Hook

A more flexible approach is to create a custom hook:

import { useEffect, useState, RefObject } from "react";

interface UseIntersectionObserverOptions {
  threshold?: number;
  rootMargin?: string;
  root?: Element | null;
}

function useIntersectionObserver<T extends Element>(
  elementRef: RefObject<T>,
  options: UseIntersectionObserverOptions = {}
): boolean {
  const [isIntersecting, setIsIntersecting] = useState(false);

  const { threshold = 0, rootMargin = "0px", root = null } = options;

  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        setIsIntersecting(entry.isIntersecting);
      },
      { threshold, rootMargin, root }
    );

    observer.observe(element);

    return () => {
      observer.unobserve(element);
    };
  }, [elementRef, threshold, rootMargin, root]);

  return isIntersecting;
}

// Usage example:
// const divRef = useRef<HTMLDivElement>(null);
// const isVisible = useIntersectionObserver(divRef, { threshold: 0.5 });

Common Use Cases for Intersection Observer

  1. Lazy loading images or content

  2. Infinite scrolling

  3. Playing/pausing videos when they enter/exit viewport

  4. Triggering animations when elements become visible

  5. Implementing scroll-spy navigation

Mutation Observer Basics

Unlike Intersection Observer, Mutation Observer monitors changes to the DOM structure itself. It detects when elements are added, removed, or attributes are changed.

How Mutation Observer Works

The Mutation Observer API works by creating an observer that watches a target node and its descendants, firing a callback whenever changes occur.

const observer = new MutationObserver(callback);
observer.observe(targetNode, options);

The callback receives an array of MutationRecord objects containing information about what changed.

Key Options for Mutation Observer

const options = {
  childList: true, // Observe direct children
  attributes: true, // Observe attributes
  characterData: true, // Observe text content
  subtree: true, // Observe all descendants
  attributeOldValue: true, // Record previous attribute value
  characterDataOldValue: true, // Record previous text
};

Using Mutation Observer in React and TypeScript

Here's how to implement a Mutation Observer in React with TypeScript:

import React, { useEffect, useRef } from "react";

interface MutationProps {
  onMutation?: (mutations: MutationRecord[]) => void;
  children: React.ReactNode;
}

const MutationComponent: React.FC<MutationProps> = ({
  onMutation,
  children,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const targetNode = containerRef.current;
    if (!targetNode) return;

    const config: MutationObserverInit = {
      attributes: true,
      childList: true,
      subtree: true,
    };

    const observer = new MutationObserver((mutations) => {
      if (onMutation) onMutation(mutations);

      // Example of processing mutations
      mutations.forEach((mutation) => {
        if (mutation.type === "childList") {
          console.log(
            "Child nodes changed:",
            mutation.addedNodes.length,
            "added"
          );
        } else if (mutation.type === "attributes") {
          console.log("Attribute changed:", mutation.attributeName);
        }
      });
    });

    observer.observe(targetNode, config);

    return () => {
      observer.disconnect();
    };
  }, [onMutation]);

  return <div ref={containerRef}>{children}</div>;
};

MutationRecord

A mutation is a change to the DOM. It can be a change to a node, an attribute, or a text node.

Each MutationRecord contains different information depending on the mutation type:

interface MutationRecord {
  type: "attributes" | "characterData" | "childList";
  target: Node;
  addedNodes: NodeList;
  removedNodes: NodeList;
  previousSibling: Node | null;
  nextSibling: Node | null;
  attributeName: string | null;
  attributeNamespace: string | null;
  oldValue: string | null;
}

For each mutation type:

  1. type: 'childList' - When child nodes are added or removed:

    • addedNodes: NodeList of newly added nodes

    • removedNodes: NodeList of removed nodes

    • target: The parent node where changes occurred

    • previousSibling: The node before the addition/removal point

    • nextSibling: The node after the addition/removal point

    • oldValue: Always null for this type

  2. type: 'attributes' - When an element's attribute changes:

    • target: The element whose attribute changed

    • attributeName: Name of the changed attribute

    • attributeNamespace: Namespace of the attribute (if applicable)

    • oldValue: Previous attribute value (only if attributeOldValue: true was specified in the observer options)

  3. type: 'characterData' - When text content changes:

    • target: The text node that changed (must be cast to CharacterData to access current .data property)

    • oldValue: Previous text content (only if characterDataOldValue: true was specified in the observer options)

Example of processing different mutation types:

function processMutations(mutations: MutationRecord[]) {
  mutations.forEach((mutation) => {
    switch (mutation.type) {
      case "childList":
        console.log(
          `${mutation.addedNodes.length} nodes added and`,
          `${mutation.removedNodes.length} nodes removed at`,
          mutation.target
        );
        break;

      case "attributes":
        console.log(
          `Attribute "${mutation.attributeName}" changed on`,
          mutation.target,
          `Previous value: ${mutation.oldValue}`
        );
        break;

      case "characterData":
        console.log(
          `Text content changed from "${mutation.oldValue}" to`,
          `"${(mutation.target as CharacterData).data}" on`,
          mutation.target
        );
        break;
    }
  });
}

Creating a Reusable Mutation Observer Hook

Here's a custom hook for more flexible usage:

import { useEffect, useRef, RefObject } from "react";

interface UseMutationObserverOptions extends MutationObserverInit {
  onMutation?: (mutations: MutationRecord[]) => void;
}

function useMutationObserver<T extends Node>(
  elementRef: RefObject<T>,
  options: UseMutationObserverOptions
): void {
  const { onMutation, ...observerOptions } = options;
  const callbackRef = useRef(onMutation);

  // Update callback ref if it changes
  useEffect(() => {
    callbackRef.current = onMutation;
  }, [onMutation]);

  useEffect(() => {
    const targetNode = elementRef.current;
    if (!targetNode) return;

    const observer = new MutationObserver((mutations) => {
      if (callbackRef.current) {
        callbackRef.current(mutations);
      }
    });

    observer.observe(targetNode, observerOptions);

    return () => {
      observer.disconnect();
    };
  }, [elementRef, JSON.stringify(observerOptions)]);
}

// Usage example:
// const divRef = useRef<HTMLDivElement>(null);
// useMutationObserver(divRef, {
//   subtree: true,
//   childList: true,
//   onMutation: (mutations) => console.log(mutations)
// });

Common Use Cases for Mutation Observer

  1. Monitoring form field changes

  2. Tracking DOM changes made by third-party libraries

  3. Implementing custom undo/redo functionality

  4. Observing when dynamically added elements appear

  5. Accessibility enhancements (e.g., announcing when important content changes)

Differences Between Intersection and Mutation Observers

Purpose

  • Intersection Observer: Detects when elements enter/exit viewport or intersect another element

  • Mutation Observer: Detects changes to DOM structure or attributes

Performance Impact

  • Intersection Observer: Very low impact, designed specifically to avoid performance problems

  • Mutation Observer: Higher impact, especially with subtree: true on large DOM trees

Triggering Conditions

  • Intersection Observer: Triggers on scrolling, resizing, or element position changes

  • Mutation Observer: Triggers on DOM modifications (adding/removing nodes, changing attributes)

Browser Support

  • Intersection Observer: IE not supported, polyfill available

  • Mutation Observer: Supported in all modern browsers including IE11

When to Use Each Observer

Use Intersection Observer when:

  • Implementing lazy loading

  • Creating infinite scroll

  • Tracking visibility for analytics

  • Implementing scroll-based animations

  • Measuring ad impressions

Use Mutation Observer when:

  • Working with content that changes dynamically

  • Integrating with libraries that modify the DOM

  • Building custom editor components

  • Needing to react to attribute changes

  • Implementing custom event systems based on DOM changes

Common Pitfalls and Best Practices

For Intersection Observer:

  1. Remember to clean up by unobserving when components unmount

  2. Choose appropriate threshold values for your use case

  3. Be careful with margin values, as they can cause premature intersection

  4. Use the once pattern by unobserving after first intersection if you only need to detect one entrance

For Mutation Observer:

  1. Limit the scope of what you're observing to improve performance

  2. Avoid triggering DOM changes inside mutation callbacks (can cause infinite loops)

  3. Be specific with options - only observe what you need

  4. Remember that style changes may not be detected unless they modify inline styles (attributes)

  5. For large applications, consider aggregating mutations before processing them

Conclusion

Both Intersection and Mutation Observers solve specific problems in web development. Intersection Observer helps detect visibility changes efficiently, while Mutation Observer tracks DOM modifications. By understanding their differences and appropriate use cases, you can implement more efficient and responsive web applications.

A

Wow. You've explained these API's beautifully. Keep up the good work.