React Custom Hook: useMutationObserver
Create a reusable React custom hook useMutationObserver that abstracts the functionality of the MutationObserver API. This hook should allow React components to easily observe changes to DOM elements and react to those changes. Mastering this hook will enable more dynamic and responsive UIs that can adapt to DOM manipulations outside of direct React control.
Problem Description
Your task is to implement a custom React hook named useMutationObserver that encapsulates the MutationObserver Web API. This hook should accept a React RefObject pointing to the DOM element to observe, a callback function to execute when mutations occur, and an optional configuration object for the observer.
Key Requirements:
-
Hook Signature: The hook should have the following signature:
function useMutationObserver( ref: React.RefObject<Element>, callback: MutationCallback, options?: MutationObserverInit ): void;ref: AReact.RefObjectpointing to the DOM element to observe.callback: AMutationCallbackfunction that will be invoked when mutations are detected. This function receives an array ofMutationRecordobjects and theMutationObserverinstance itself.options: An optionalMutationObserverInitobject to configure which mutations to observe (e.g.,childList,attributes,subtree,characterData).
-
Observer Initialization: The hook should create a new
MutationObserverinstance internally. -
Observation Start/Stop:
- The observer should start observing the target element when the component mounts or when the
reforcallbackchanges. - The observer should stop observing when the component unmounts or when the
reforcallbackchanges (to ensure proper cleanup and prevent memory leaks).
- The observer should start observing the target element when the component mounts or when the
-
Cleanup: Implement proper cleanup by calling
observer.disconnect()when the hook is no longer needed. -
Type Safety: The hook and its parameters should be strongly typed using TypeScript.
Expected Behavior:
When a MutationObserver is active for a given element, and the specified mutations occur (based on the options), the provided callback function should be executed with the relevant mutation data.
Examples
Example 1: Observing Child List Changes
Consider a component that displays a list of items and you want to know when items are added or removed from a specific container.
Input:
import React, { useRef, useState, useEffect } from 'react';
import { useMutationObserver } from './useMutationObserver'; // Assuming hook is in this file
const ChildListObserver: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const [changes, setChanges] = useState<string[]>([]);
const mutationCallback: MutationCallback = (mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node instanceof Element) {
setChanges(prev => [...prev, `Added: ${node.textContent}`]);
}
});
mutation.removedNodes.forEach(node => {
if (node instanceof Element) {
setChanges(prev => [...prev, `Removed: ${node.textContent}`]);
}
});
}
});
};
useMutationObserver(containerRef, mutationCallback, {
childList: true,
subtree: true, // Observe changes in descendants too
});
const addItem = () => {
const newItem = document.createElement('div');
newItem.textContent = `Item ${changes.length + 1}`;
containerRef.current?.appendChild(newItem);
};
const removeItem = () => {
if (containerRef.current && containerRef.current.lastChild) {
containerRef.current.removeChild(containerRef.current.lastChild);
}
};
return (
<div>
<button onClick={addItem}>Add Item</button>
<button onClick={removeItem}>Remove Item</button>
<div ref={containerRef} style={{ border: '1px solid black', padding: '10px', minHeight: '50px' }}>
{/* Initial content can go here, or it can be dynamically added */}
</div>
<h3>Detected Changes:</h3>
<ul>
{changes.map((change, index) => (
<li key={index}>{change}</li>
))}
</ul>
</div>
);
};
export default ChildListObserver;
Output (after clicking "Add Item" twice, then "Remove Item" once):
Detected Changes:
Added: Item 1
Added: Item 2
Removed: Item 2
Explanation: The useMutationObserver hook is used to monitor the containerRef for childList changes. When "Add Item" is clicked, new div elements are appended, triggering the mutationCallback. When "Remove Item" is clicked, the last div is removed, also triggering the callback. The state changes is updated to reflect these additions and removals.
Example 2: Observing Attribute Changes
Imagine you want to react when a specific attribute of an element changes, for example, a data-status attribute on a task item.
Input:
import React, { useRef, useState } from 'react';
import { useMutationObserver } from './useMutationObserver'; // Assuming hook is in this file
const AttributeObserver: React.FC = () => {
const taskRef = useRef<HTMLDivElement>(null);
const [status, setStatus] = useState('pending');
const mutationCallback: MutationCallback = (mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-status') {
const newStatus = (mutation.target as Element).getAttribute('data-status');
setStatus(newStatus || 'unknown');
}
});
};
useMutationObserver(taskRef, mutationCallback, {
attributes: true, // Observe attribute changes
attributeOldValue: false, // We only care about the new value
// attributeFilter: ['data-status'] // More specific if needed
});
const updateStatus = () => {
const nextStatus = status === 'pending' ? 'completed' : 'pending';
taskRef.current?.setAttribute('data-status', nextStatus);
};
return (
<div>
<div
ref={taskRef}
data-status={status}
style={{ border: '1px solid blue', padding: '10px', margin: '10px' }}
>
Task Item
</div>
<button onClick={updateStatus}>
Change Status (Current: {status})
</button>
<h3>Observed Status: {status}</h3>
</div>
);
};
export default AttributeObserver;
Output (after clicking "Change Status" once):
Observed Status: completed
Explanation: The useMutationObserver hook monitors the taskRef for attributes changes. When the "Change Status" button is clicked, the data-status attribute is updated. The mutationCallback detects this change and updates the component's state, reflecting the new status.
Constraints
- The
MutationObserverAPI must be used. Direct manipulation of React state based on user events is not the primary focus of this challenge, but rather reacting to DOM changes that might occur from other sources. - The hook must handle the lifecycle of the
MutationObservercorrectly, ensuring it's connected when needed and disconnected on unmount or when dependencies change. - The hook should be implemented in TypeScript.
Notes
- Consider how the
useMutationObserverhook will behave if therefobject's.currentproperty isnullinitially. The observer should not be initialized until therefpoints to a valid DOM element. - The
callbackfunction passed to theMutationObserverhas a specific signature. Ensure your implementation adheres to this. - Think about potential performance implications. If
subtree: trueandattributes: trueare used on a very complex DOM, it could lead to performance issues. While not a strict constraint for this challenge, it's a good practice to be aware of. - The
optionsobject forMutationObserverInitcan be complex. The hook should correctly pass this object through.