@react-hookz/web
Version:
React hooks done right, for browser and SSR.
95 lines (94 loc) • 3.54 kB
JavaScript
import { useEffect } from 'react';
import { useSyncedRef } from '../useSyncedRef/index.js';
import { isBrowser } from '../util/const.js';
let observerSingleton;
function getResizeObserver() {
if (!isBrowser) {
return undefined;
}
if (observerSingleton) {
return observerSingleton;
}
const callbacks = new Map();
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const cbs = callbacks.get(entry.target);
if (cbs === undefined || cbs.size === 0) {
continue;
}
for (const cb of cbs) {
setTimeout(() => {
cb(entry);
}, 0);
}
}
});
observerSingleton = {
observer,
subscribe(target, callback) {
let cbs = callbacks.get(target);
if (!cbs) {
// If target has no observers yet - register it
cbs = new Set();
callbacks.set(target, cbs);
observer.observe(target);
}
// As Set is duplicate-safe - simply add callback on each call
cbs.add(callback);
},
unsubscribe(target, callback) {
const cbs = callbacks.get(target);
// Else branch should never occur in case of normal execution
// because callbacks map is hidden in closure - it is impossible to
// simulate situation with non-existent `cbs` Set
if (cbs) {
// Remove current observer
cbs.delete(callback);
if (cbs.size === 0) {
// If no observers left unregister target completely
callbacks.delete(target);
observer.unobserve(target);
}
}
},
};
return observerSingleton;
}
/**
* Invokes a callback whenever ResizeObserver detects a change to target's size.
*
* @param target React reference or Element to track.
* @param callback Callback that will be invoked on resize.
* @param enabled Whether resize observer is enabled or not.
*/
export function useResizeObserver(target, callback, enabled = true) {
const ro = enabled && getResizeObserver();
const cb = useSyncedRef(callback);
const tgt = target && 'current' in target ? target.current : target;
useEffect(() => {
// This secondary target resolve required for case when we receive ref object, which, most
// likely, contains null during render stage, but already populated with element during
// effect stage.
const tgt = target && 'current' in target ? target.current : target;
if (!ro || !tgt) {
return;
}
// As unsubscription in internals of our ResizeObserver abstraction can
// happen a bit later than effect cleanup invocation - we need a marker,
// that this handler should not be invoked anymore
let subscribed = true;
const handler = (...args) => {
// It is reinsurance for the highly asynchronous invocations, almost
// impossible to achieve in tests, thus excluding from LOC
if (subscribed) {
cb.current(...args);
}
};
ro.subscribe(tgt, handler);
return () => {
subscribed = false;
ro.unsubscribe(tgt, handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tgt, ro]);
}