@crimson_dev/use-resize-observer
v1.0.0
Published
Zero-dependency, Worker-native, ESNext-first React 19 ResizeObserver hook
Downloads
700
Maintainers
Readme
Zero-dependency, Worker-native, ESNext-first React 19 ResizeObserver hook
Quick Start
npm install @crimson_dev/use-resize-observerimport { useResizeObserver } from '@crimson_dev/use-resize-observer';
function ResponsiveCard() {
const { ref, width, height } = useResizeObserver<HTMLDivElement>();
return (
<div ref={ref}>
{width} × {height}
</div>
);
}Why This Exists
Most resize hooks create one ResizeObserver per component. At scale, that means hundreds of observers competing for the main thread. This library uses a shared observer pool — one ResizeObserver per document root — with requestAnimationFrame batching and React startTransition wrapping. The result: 100 elements resizing = 1 render cycle.
Highlights
Entry Points
// Main — primary hook
import { useResizeObserver } from '@crimson_dev/use-resize-observer';
// Multi-element — observe N elements with 1 hook
import { useResizeObserverEntries } from '@crimson_dev/use-resize-observer';
// Factory — framework-agnostic, imperative API
import { createResizeObserver } from '@crimson_dev/use-resize-observer';
// Worker — SAB-based measurements for cross-thread sharing
import { useResizeObserverWorker } from '@crimson_dev/use-resize-observer/worker';
// Core — EventTarget-based, any framework
import { createResizeObservable } from '@crimson_dev/use-resize-observer/core';
// Server — SSR/RSC safe
import { createServerResizeObserverMock } from '@crimson_dev/use-resize-observer/server';
// Shim — polyfill for legacy browsers
import '@crimson_dev/use-resize-observer/shim';API
useResizeObserver<T>(options?)
const { ref, width, height, entry } = useResizeObserver<HTMLDivElement>({
box: 'content-box', // 'content-box' | 'border-box' | 'device-pixel-content-box'
ref: externalRef, // optional external ref
root: shadowRoot, // optional Document | ShadowRoot
onResize: (entry) => {}, // stable callback (no useCallback needed)
});| Return | Type | Description |
|--------|------|-------------|
| ref | RefObject<T \| null> | Attach to the element to observe |
| width | number \| undefined | Inline size of the observed box |
| height | number \| undefined | Block size of the observed box |
| entry | ResizeObserverEntry \| undefined | Raw entry from the observer |
useResizeObserverEntries(refs, options?)
const entries = useResizeObserverEntries([ref1, ref2, ref3], {
box: 'border-box',
});
// entries: Map<Element, { width, height, entry }>createResizeObserver(options?)
using observer = createResizeObserver({ box: 'border-box' });
observer.observe(element, (entry) => console.log(entry));
// Automatically cleaned up via Symbol.disposeArchitecture
┌─────────────────────────────────────────────────┐
│ Your Components │
│ useResizeObserver() useResizeObserverEntries() │
└──────────────────────┬──────────────────────────┘
│
┌────────▼────────┐
│ ObserverPool │ 1 per document root
│ WeakMap<Element,│ Single-callback fast path
│ Callback|Set> │ FinalizationRegistry
└────────┬────────┘
│
┌────────▼────────┐
│ RafScheduler │ Double-buffered Maps
│ XOR swap │ Zero-alloc flush
│ last-write-wins│ requestAnimationFrame
└────────┬────────┘
│
┌────────▼────────┐
│ startTransition │ Non-urgent batched update
│ setState() │ 1 render per frame
└─────────────────┘Comparison
| | use-resize-observer@9 | @crimson_dev/use-resize-observer |
|---|---|---|
| Bundle | ~800B | 1.11 kB (pool + scheduler + hook) |
| React | 16.8+ | 19.3+ with Compiler |
| Module | CJS + ESM | ESM only |
| TypeScript | 4.x | 6.0 strict |
| Observer model | 1 per component | Shared pool |
| Worker mode | — | SharedArrayBuffer |
| Box models | content-box only | All 3 |
| GC cleanup | Manual | Automatic |
| Batching | None | rAF + startTransition |
Requirements
- Node ≥ 25.0.0
- React ≥ 19.3.0
- TypeScript ≥ 6.0 (recommended)
- Browser with native
ResizeObserver(or use the/shimentry)
License
MIT — Crimson Dev
