@usefy/use-mutation-observer
v0.25.1
Published
A React hook for observing DOM mutations using the MutationObserver API
Maintainers
Readme
Overview
useMutationObserver is part of the @usefy ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It wraps the browser's MutationObserver API so you can react to DOM mutations — child nodes added/removed, attributes changing, or character data changing — by attaching a single callback ref.
It is the low-level observer primitive, a sibling to @usefy/use-resize-observer and @usefy/use-intersection-observer, and follows the same ref / enabled / onXxx / updateState conventions.
Features
- Callback ref — attach
refto a node to observe it; passnullto disconnect - Full
MutationObserverInitsurface —childList,attributes,attributeFilter,attributeOldValue,characterData,characterDataOldValue,subtree - Sensible default — defaults to
childList: true(and impliesattributes/characterDatafrom their sub-options) soobserve()never throws - Callback + state — react via
onMutation, or the reactiverecordsstate, or both;updateState: falsefor zero-re-render callback-only mode - Stable callback —
onMutationis stored in a ref, so changing its identity every render does not tear down and re-create the observer enabledtoggle & manual controls — pause observation, or driveobserve/disconnect/takeRecordsyourself- SSR-safe — no
MutationObserver/DOM access on the server; degrades toisSupported: false - StrictMode / concurrent-safe — clean connect/disconnect with no leaked observers
- TypeScript-first — full type inference and exported types
- Tiny & tree-shakeable — zero dependencies, published as its own package
Installation
# npm
npm install @usefy/use-mutation-observer
# yarn
yarn add @usefy/use-mutation-observer
# pnpm
pnpm add @usefy/use-mutation-observerRequires React 18 or 19 (peerDependencies: "react": "^18.0.0 || ^19.0.0").
Quick Start
import { useMutationObserver } from "@usefy/use-mutation-observer";
function Watched() {
const { ref, records } = useMutationObserver<HTMLDivElement>({
childList: true,
subtree: true,
onMutation: (mutations) => {
for (const m of mutations) console.log(m.type, m.target);
},
});
return <div ref={ref}>{records.length} recent mutations</div>;
}API
useMutationObserver<T>(options?)
Observes the element attached via the returned ref for DOM mutations.
Options — UseMutationObserverOptions<T>
| Option | Type | Default | Description |
| ----------------------- | ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------- |
| childList | boolean | true* | Observe additions/removals of the target's child nodes |
| attributes | boolean | false | Observe attribute changes (implied true when attributeFilter/attributeOldValue is set)|
| attributeFilter | string[] | — | Only observe the named attributes (implies attributes: true) |
| attributeOldValue | boolean | false | Record the previous attribute value in MutationRecord.oldValue (implies attributes) |
| characterData | boolean | false | Observe character-data changes |
| characterDataOldValue | boolean | false | Record the previous character data in oldValue (implies characterData) |
| subtree | boolean | false | Extend observation to the whole subtree, not just direct children |
| onMutation | (mutations: MutationRecord[], observer: MutationObserver) => void | — | Callback fired with each batch of records (stored in a ref — safe to pass inline) |
| enabled | boolean | true | When false, disconnects and stops reporting; flip back to true to re-observe |
| updateState | boolean | true | Whether to mirror the latest batch into the records state. false = callback-only |
* childList defaults to true only when none of childList/attributes/characterData is enabled — the DOM API requires at least one.
Returns — UseMutationObserverReturn<T>
| Property | Type | Description |
| ------------- | --------------------------------- | ----------------------------------------------------------------------- |
| ref | (element: T \| null) => void | Callback ref to attach to the target; null disconnects |
| records | readonly MutationRecord[] | The latest batch of records (empty until the first mutation) |
| isSupported | boolean | Whether the MutationObserver API is available |
| isObserving | boolean | Whether the hook is currently observing an element |
| observe | (element: T) => void | Manually start observing an element (escape hatch alongside ref) |
| disconnect | () => void | Disconnect the observer, stopping all observation |
| takeRecords | () => MutationRecord[] | Flush and return any queued-but-undelivered records |
Note:
MutationObserverhas no per-targetunobserve;disconnect()(dropping all observation) is the only way to stop, matching the native API.
disconnect()is terminal: after calling it, re-attaching the samerefdoes not restart observation (the ref only re-observes when the element changes, and the internal observer is torn down). To resume, callobserve(element)again, or remount the component.
Also exported: isMutationObserverSupported() and resolveMutationConfig(options) (pure helpers), the EMPTY_RECORDS sentinel, and the UseMutationObserverOptions, UseMutationObserverReturn, OnMutationCallback types.
Examples
Callback-only mode (zero re-renders)
import { useMutationObserver } from "@usefy/use-mutation-observer";
function ClassWatcher() {
const { ref } = useMutationObserver({
attributeFilter: ["class"],
attributeOldValue: true,
updateState: false, // skip state, handle everything in the callback
onMutation: (mutations) => {
const last = mutations[mutations.length - 1];
console.log(last?.attributeName, "was", last?.oldValue);
},
});
return <div ref={ref} className="box" />;
}Pause and resume observation
import { useState } from "react";
import { useMutationObserver } from "@usefy/use-mutation-observer";
function Toggleable() {
const [enabled, setEnabled] = useState(true);
const { ref, records } = useMutationObserver({ childList: true, enabled });
return (
<div>
<button onClick={() => setEnabled((e) => !e)}>
{enabled ? "Pause" : "Resume"} observing
</button>
<div ref={ref}>{records.length} recent child mutations</div>
</div>
);
}Testing
📊 View Detailed Coverage Report (GitHub Pages) — 62 tests, 99% statement coverage.
useMutationObserver.test.ts— 44 tests for hook behavior (observation, options passthrough, callback-ref,enabled, stable callback, manual control, cleanup, StrictMode, unsupported env)utils.test.ts— 18 tests for the config resolver and helpers
License
MIT © mirunamu
This package is part of the usefy monorepo.
