UNPKG

@react-hookz/web

Version:

React hooks done right, for browser and SSR.

95 lines (94 loc) 3.54 kB
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]); }