@acusti/use-bounding-client-rect
Version:
React hook that returns the boundingClientRect for an element and triggers an update when those dimensions change
139 lines • 5.08 kB
JavaScript
import * as React from 'react';
const { useEffect, useState } = React;
const noop = () => { }; // eslint-disable-line @typescript-eslint/no-empty-function
const RESIZE_OBSERVER_STUB = {
disconnect: noop,
observe: noop,
unobserve: noop,
};
const EMPTY_RECT = Object.freeze({
bottom: undefined,
left: undefined,
right: undefined,
top: undefined,
});
const EMPTY_REFS = Object.freeze({
boundingClientRect: EMPTY_RECT,
maybeCleanupElement: noop,
maybeCleanupTimer: null,
renderTimeSetters: new Set(),
retryCount: 0,
scheduleUpdate: noop,
updateBoundingClientRect: noop,
updaterFrameID: null,
});
const MINUTE = 60 * 1000;
const refsByElement = new WeakMap();
let resizeObserver = RESIZE_OBSERVER_STUB;
if (typeof ResizeObserver === 'function') {
resizeObserver = new ResizeObserver((entries, observer) => {
for (const entry of entries) {
const element = entry.target;
const refs = refsByElement.get(element);
if (!refs) {
observer.unobserve(element);
return;
}
refs.scheduleUpdate();
}
});
}
const initializeUpdateHandlers = (element) => {
const refs = refsByElement.get(element);
if (!refs)
return;
// If update handlers are already initialized, there’s no more work to do
if (refs.scheduleUpdate != null && refs.scheduleUpdate !== noop)
return;
refs.updateBoundingClientRect = () => {
refs.updaterFrameID = null;
const rect = element.getBoundingClientRect();
// If element has no width or height, its layout is still being calculated
if (!rect.height && !rect.width) {
if (refs.retryCount < 10) {
refs.retryCount++;
refs.updaterFrameID = requestAnimationFrame(refs.updateBoundingClientRect);
}
return;
}
if (refs.retryCount) {
refs.retryCount = 0;
}
// Only update boundingClientRect if at least one of the values has changed
if (refs.boundingClientRect.bottom === rect.bottom &&
refs.boundingClientRect.left === rect.left &&
refs.boundingClientRect.right === rect.right &&
refs.boundingClientRect.top === rect.top) {
return;
}
refs.boundingClientRect = {
bottom: rect.bottom,
left: rect.left,
right: rect.right,
top: rect.top,
};
const renderTime = typeof Date.now === 'function' ? Date.now() : 0;
for (const setRenderTime of refs.renderTimeSetters) {
setRenderTime(renderTime);
}
};
refs.scheduleUpdate = () => {
// Use requestAnimationFrame-based throttling to prevent ResizeObserver loop limit exceeded
if (refs.updaterFrameID != null)
return;
refs.updaterFrameID = requestAnimationFrame(refs.updateBoundingClientRect);
};
refs.maybeCleanupElement = () => {
if (refs.maybeCleanupTimer != null) {
clearTimeout(refs.maybeCleanupTimer);
refs.maybeCleanupTimer = null;
}
// If components are using this element and element still in DOM, no cleanup needed
if (refs.renderTimeSetters.size && element.closest('html')) {
refs.maybeCleanupTimer = setTimeout(refs.maybeCleanupElement, MINUTE);
return;
}
refsByElement.delete(element);
resizeObserver.unobserve(element);
};
refs.maybeCleanupTimer = setTimeout(refs.maybeCleanupElement, MINUTE);
};
const cleanupHookInstance = (element, setRenderTime) => {
const refs = refsByElement.get(element);
if (!refs)
return;
refs.renderTimeSetters.delete(setRenderTime);
refs.maybeCleanupElement();
};
const useBoundingClientRect = (element) => {
var _a, _b;
// Flip the bit to trigger a new return value from this hook
const [, setRenderTime] = useState(0);
// If element isn’t in our refs map, initialize it
let isInitializing = false;
let refs = (_a = (element && refsByElement.get(element))) !== null && _a !== void 0 ? _a : null;
if (element && !refs) {
isInitializing = true;
refs = Object.assign(Object.assign({}, EMPTY_REFS), { renderTimeSetters: new Set() });
refsByElement.set(element, refs);
initializeUpdateHandlers(element);
resizeObserver.observe(element);
}
if (refs) {
refs.renderTimeSetters.add(setRenderTime);
if (isInitializing) {
refs.updateBoundingClientRect();
}
}
useEffect(() => {
if (!element)
return noop;
const _element = element;
return () => {
cleanupHookInstance(_element, setRenderTime);
};
}, [element]);
return (_b = refs === null || refs === void 0 ? void 0 : refs.boundingClientRect) !== null && _b !== void 0 ? _b : EMPTY_RECT;
};
export default useBoundingClientRect;
//# sourceMappingURL=useBoundingClientRect.js.map