A singleton wrapper for the ResizeObserver API that optimizes performance and simplifies usage.
The browser's built-in ResizeObserver API is powerful but can lead to performance issues when many observers are created, often one for each element to observe. Common scenarios where this happens include:
- Multiple components independently observing the same DOM elements
- Libraries that each create their own ResizeObserver instances
- Complex UIs that need to react to size changes of many elements
This package solves these problems by:
- Sharing a single ResizeObserver instance across your application and libraries
- Managing multiple handlers for the same elements
- Supporting different box options with separate observer instances
- Providing a clean disposal API to prevent memory leaks
- Supporting polyfills for older browsers
# npm
npm install resize-observer-singleton
# yarn
yarn add resize-observer-singleton
# pnpm
pnpm add resize-observer-singleton
import { ResizeObserverSingleton } from "resize-observer-singleton";
// Get a shared instance (defaults to 'border-box')
const resizeObserver = ResizeObserverSingleton.getInstance();
// Observe an element with a handler
const element = document.querySelector(".my-element");
const unobserve = resizeObserver.observe(element, (entry) => {
console.log("Element resized:", entry.contentRect);
});
// Later, when you no longer need to observe:
unobserve();
// Or manually:
resizeObserver.unobserve(element, handler);
// To completely disconnect all observations (rarely needed):
resizeObserver.disconnect();
import { useEffect, useRef } from "react";
import { ResizeObserverSingleton } from "resize-observer-singleton";
function ResizeAwareComponent() {
const ref = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const element = ref.current;
if (!element) return;
// You can specify box options: 'border-box', 'content-box', 'device-pixel-content-box'
const resizeObserver = ResizeObserverSingleton.getInstance("content-box");
const handleResize = (entry) => {
const { width, height } = entry.contentRect;
setDimensions({ width, height });
};
// The observe method returns an unsubscribe function
const unobserve = resizeObserver.observe(element, handleResize);
// Clean up when component unmounts
return unobserve;
}, []);
return (
<div ref={ref} className="resizable-component">
Width: {dimensions.width}px, Height: {dimensions.height}px
</div>
);
}
import { useEffect, useRef, useState } from "react";
import { ResizeObserverSingleton } from "resize-observer-singleton";
export function useResizeObserver(box = "border-box") {
const ref = useRef(null);
const [entry, setEntry] = useState(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const resizeObserver = ResizeObserverSingleton.getInstance(box);
const unobserve = resizeObserver.observe(element, (resizeEntry) => {
setEntry(resizeEntry);
});
return unobserve;
}, [box]);
return [ref, entry];
}
// Usage
function MyComponent() {
const [ref, entry] = useResizeObserver();
return (
<div ref={ref} style={{ width: "100%", height: "100%" }}>
{entry && (
<div>
Width: {entry.contentRect.width}px Height: {entry.contentRect.height}
px
</div>
)}
</div>
);
}
For older browsers, you can register a polyfill implementation:
import ResizeObserverPolyfill from "resize-observer-polyfill";
import {
registerResizeObserverImplementation,
ResizeObserverSingleton,
} from "resize-observer-singleton";
// Register the polyfill before using the singleton
registerResizeObserverImplementation(ResizeObserverPolyfill);
// Now you can use ResizeObserverSingleton as normal
const resizeObserver = ResizeObserverSingleton.getInstance();
// Create instances with different box options
const borderBoxObserver = ResizeObserverSingleton.getInstance("border-box");
const contentBoxObserver = ResizeObserverSingleton.getInstance("content-box");
const devicePixelObserver = ResizeObserverSingleton.getInstance(
"device-pixel-content-box",
);
// Each will maintain its own shared ResizeObserver
MIT