react-resize-detector
Version:
React resize detector
163 lines (162 loc) • 7.21 kB
JavaScript
import*as React from'react';import {useRef,useState,useCallback,useEffect}from'react';import debounce from'lodash/debounce';import throttle from'lodash/throttle';/**
* Wraps the resize callback with a lodash debounce / throttle based on the refresh mode
*/
const patchResizeCallback = (resizeCallback, refreshMode, refreshRate, refreshOptions) => {
switch (refreshMode) {
case 'debounce':
return debounce(resizeCallback, refreshRate, refreshOptions);
case 'throttle':
return throttle(resizeCallback, refreshRate, refreshOptions);
default:
return resizeCallback;
}
};
/**
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
* prop or avoid re-executing effects when passed as a dependency
*/
const useCallbackRef =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(callback) => {
const callbackRef = React.useRef(callback);
React.useEffect(() => {
callbackRef.current = callback;
});
return React.useMemo(() => ((...args) => { var _a; return (_a = callbackRef.current) === null || _a === void 0 ? void 0 : _a.call(callbackRef, ...args); }), []);
};
/** `useRef` hook doesn't handle conditional rendering or dynamic ref changes.
* This hook creates a proxy that ensures that `refElement` is updated whenever the ref is changed. */
const useRefProxy =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(targetRef) => {
// we are going to use this ref to store the last element that was passed to the hook
const [refElement, setRefElement] = React.useState((targetRef === null || targetRef === void 0 ? void 0 : targetRef.current) || null);
// if targetRef is passed, we need to update the refElement
// we have to use setTimeout because ref get assigned after the hook is called
// in the future releases we are going to remove targetRef and force users to use ref returned by the hook
if (targetRef) {
setTimeout(() => {
if (targetRef.current !== refElement) {
setRefElement(targetRef.current);
}
}, 0);
}
// this is a memo that will be called every time the ref is changed
// This proxy will properly call setState either when the ref is called as a function or when `.current` is set
// we call setState inside to trigger rerender
const refProxy = React.useMemo(() => new Proxy(node => {
if (node !== refElement) {
setRefElement(node);
}
}, {
get(target, prop) {
if (prop === 'current') {
return refElement;
}
return target[prop];
},
set(target, prop, value) {
if (prop === 'current') {
setRefElement(value);
}
else {
target[prop] = value;
}
return true;
}
}), [refElement]);
return { refProxy, refElement, setRefElement };
};
/** Calculates the dimensions of the element based on the current box model.
* @see https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model
*/
const getDimensions = (entry, box) => {
// Value Border Padding Inner Content
// ---------------------------------------------------
// 'border-box' Yes Yes Yes
// 'content-box' No No Yes
// undefined No No? Yes
if (box === 'border-box') {
return {
width: entry.borderBoxSize[0].inlineSize,
height: entry.borderBoxSize[0].blockSize
};
}
if (box === 'content-box') {
return {
width: entry.contentBoxSize[0].inlineSize,
height: entry.contentBoxSize[0].blockSize
};
}
return {
width: entry.contentRect.width,
height: entry.contentRect.height
};
};// eslint-disable-next-line @typescript-eslint/no-explicit-any
function useResizeDetector({ skipOnMount = false, refreshMode, refreshRate = 1000, refreshOptions, handleWidth = true, handleHeight = true, targetRef, observerOptions, onResize } = {}) {
// If `skipOnMount` is enabled, skip the first resize event
const skipResize = useRef(skipOnMount);
// Wrap the `onResize` callback with a ref to avoid re-renders
const onResizeRef = useCallbackRef(onResize);
const [size, setSize] = useState({
width: undefined,
height: undefined
});
// Create a proxy ref to handle conditional rendering and dynamic ref changes of the target element
const { refProxy, refElement } = useRefProxy(targetRef);
const { box } = observerOptions || {};
const resizeCallback = useCallback((entries) => {
if (!handleWidth && !handleHeight)
return;
if (skipResize.current) {
skipResize.current = false;
return;
}
// Only update the size if one of the observed dimensions has changed
const shouldSetSize = (prevSize, nextSize) => (handleWidth && prevSize.width !== nextSize.width) || (handleHeight && prevSize.height !== nextSize.height);
entries.forEach(entry => {
const dimensions = getDimensions(entry, box);
setSize(prevSize => {
if (!shouldSetSize(prevSize, dimensions))
return prevSize;
onResizeRef === null || onResizeRef === void 0 ? void 0 : onResizeRef({
width: dimensions.width,
height: dimensions.height,
entry
});
return dimensions;
});
});
}, [handleWidth, handleHeight, skipResize, box]);
// Throttle/Debounce the resize event if refreshMode is configured
const resizeHandler = useCallback(patchResizeCallback(resizeCallback, refreshMode, refreshRate, refreshOptions), [
resizeCallback,
refreshMode,
refreshRate,
refreshOptions
]);
// Attach ResizeObserver to the element
useEffect(() => {
let resizeObserver;
if (refElement) {
resizeObserver = new window.ResizeObserver(resizeHandler);
resizeObserver.observe(refElement, observerOptions);
}
// If refElement is not available, reset the size
else if (size.width || size.height) {
onResizeRef === null || onResizeRef === void 0 ? void 0 : onResizeRef({
width: null,
height: null,
entry: null
});
setSize({ width: undefined, height: undefined });
}
// Disconnect the ResizeObserver when the component is unmounted
return () => {
var _a, _b, _c;
(_a = resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect) === null || _a === void 0 ? void 0 : _a.call(resizeObserver);
(_c = (_b = resizeHandler).cancel) === null || _c === void 0 ? void 0 : _c.call(_b);
};
}, [resizeHandler, refElement]);
return Object.assign({ ref: refProxy }, size);
}export{useResizeDetector};//# sourceMappingURL=index.esm.js.map