useMutationObserver
EditObserves DOM mutations for an element assigned through a callback ref.
import { useCallback, useEffect, useLayoutEffect, useRef, useState,} from "react";
import type { RefCallback } from "react";
export interface UseMutationObserverOptions extends MutationObserverInit { enabled?: boolean;}
export interface UseMutationObserverReturn<ElementType extends Node> { isSupported: boolean; ref: RefCallback<ElementType>;}
interface ObservedNode<ElementType extends Node> { node: ElementType | null; version: number;}
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
/** Observes mutations for a callback-ref node with the latest callback. */export const useMutationObserver = <ElementType extends Node = HTMLElement>( callback: MutationCallback, options: UseMutationObserverOptions = { childList: true },): UseMutationObserverReturn<ElementType> => { const { enabled = true, attributeFilter, attributeOldValue, attributes, characterData, characterDataOldValue, childList, subtree, } = options; const [observedNode, setObservedNode] = useState< ObservedNode<ElementType> >({ node: null, version: 0 }); const { node, version: nodeVersion } = observedNode; const nodeRef = useRef<ElementType | null>(null); const nodeVersionRef = useRef(0); const callbackRef = useRef(callback); useIsomorphicLayoutEffect(() => { callbackRef.current = callback; }, [callback]); const ref = useCallback<RefCallback<ElementType>>((nextNode) => { nodeRef.current = nextNode; nodeVersionRef.current += 1; setObservedNode({ node: nextNode, version: nodeVersionRef.current }); }, []); const isSupported = typeof window !== "undefined" && typeof MutationObserver === "function"; const attributeFilterKey = attributeFilter?.join("\u0000"); const effectiveAttributes = attributes ?? (attributeFilter !== undefined || attributeOldValue === true); const effectiveCharacterData = characterData ?? characterDataOldValue === true; const effectiveChildList = childList ?? (!effectiveAttributes && !effectiveCharacterData); if ( attributes === false && (attributeFilter !== undefined || attributeOldValue === true) ) { throw new TypeError( "attributes cannot be false when attributeFilter or attributeOldValue is enabled", ); } if (characterData === false && characterDataOldValue === true) { throw new TypeError( "characterData cannot be false when characterDataOldValue is enabled", ); } if (!effectiveAttributes && !effectiveCharacterData && !effectiveChildList) { throw new TypeError("At least one mutation observation mode must be enabled"); }
useEffect(() => { if (!enabled || !isSupported || node === null) return; let active = true; const observer = new MutationObserver((records, currentObserver) => { if ( !active || nodeRef.current !== node || nodeVersionRef.current !== nodeVersion ) { return; } Reflect.apply(callbackRef.current, undefined, [ records, currentObserver, ]); }); observer.observe(node, { ...(attributeFilter === undefined ? {} : { attributeFilter }), ...(attributeOldValue === undefined ? {} : { attributeOldValue }), attributes: effectiveAttributes, characterData: effectiveCharacterData, ...(characterDataOldValue === undefined ? {} : { characterDataOldValue }), childList: effectiveChildList, ...(subtree === undefined ? {} : { subtree }), }); return () => { active = false; observer.disconnect(); }; }, [ attributeFilterKey, attributeOldValue, effectiveAttributes, effectiveCharacterData, characterDataOldValue, effectiveChildList, enabled, isSupported, node, nodeVersion, subtree, ]);
return { isSupported, ref };};Download
Section titled “Download”wget -O src/hooks/useMutationObserver.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useMutationObserver.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useMutationObserver.ts" src/hooks/useMutationObserver.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useState } from "react";
import { useMutationObserver } from "./useMutationObserver";
export const MutationObserverExample = () => { const [items, setItems] = useState<string[]>([]); const [mutations, setMutations] = useState(0); const { ref } = useMutationObserver<HTMLUListElement>( (records) => setMutations((count) => count + records.length), { childList: true }, );
return ( <div> <button type="button" onClick={() => setItems((list) => [...list, "Item"])}> Add item </button> <p>Observed mutations: {mutations}</p> <ul ref={ref}>{items.map((item, index) => <li key={index}>{item}</li>)}</ul> </div> );};callback(MutationCallback) — Receives records and the active observer.options— Native mutation options plusenabled. When no observation mode is supplied,childListdefaults totrue.
Returns
Section titled “Returns”The callback ref and isSupported.