Skip to content

Instantly share code, notes, and snippets.

@ferdaber
Created April 26, 2020 02:53
Show Gist options
  • Select an option

  • Save ferdaber/eed76efbd322774eadfebd9da2d4ff23 to your computer and use it in GitHub Desktop.

Select an option

Save ferdaber/eed76efbd322774eadfebd9da2d4ff23 to your computer and use it in GitHub Desktop.
import {
createContext,
useRef,
useContext,
useCallback,
useLayoutEffect,
useEffect,
ReactNode,
createElement,
} from "react";
import { useRerender } from "./use-state-variants";
export type MeasurableRefObject = {
readonly current?: {
clientHeight: number;
clientWidth: number;
getBoundingClientRect(): { x: number; y: number };
} | null;
};
export type Dimensions = {
height?: number;
width?: number;
x?: number;
y?: number;
};
const ContinuousPollingContext = createContext(false);
/**
* A hook that measures the dimensions of a ref's value and updates with every window resize
* and with every component update.
*
* Use `useDimensions.windowRef` as the ref to measure the window's dimensions
*
* Use `continuousPolling` to force the hook to remeasure with every animation frame
*
* Any usage of the hook in a component that is inside `useDimensions.ContinuousPollingProvider`
* will also remeasure with every animation frame
* @example
* <useDimensions.ContinuousPollingProvider>
* <ComponentThatUsesUseDimensions /> // will rerender with every animation frame
* </useDimensions.ContinuousPollingProvider>
*/
export function useDimensions(
measurableRef: MeasurableRefObject,
continuousPolling = false
) {
const rerender = useRerender();
const dimensions = useRef<{
height?: number;
width?: number;
x?: number;
y?: number;
}>({});
if (useContext(ContinuousPollingContext)) {
continuousPolling = true;
}
const measure = useCallback(() => {
if (!measurableRef.current) return;
const { height, width, x, y } = dimensions.current;
const refHeight = measurableRef.current.clientHeight;
const refWidth = measurableRef.current.clientWidth;
const { x: refX, y: refY } = measurableRef.current.getBoundingClientRect();
dimensions.current.height = refHeight;
dimensions.current.width = refWidth;
dimensions.current.x = refX;
dimensions.current.y = refY;
if (refHeight !== height || refWidth !== width || refX !== x || refY !== y)
rerender();
}, [measurableRef, rerender]);
useLayoutEffect(() => {
try {
measure();
} catch (e) {
// https://github.com/facebook/react/blob/da834083cccb6ef942f701c6b6cecc78213196a8/packages/react-reconciler/src/ReactFiberWorkLoop.js#L2614
console.error(
"useDimensions crashed while attempting to re-measure. This is likely an infinite loop."
);
console.error(e);
}
});
useEffect(() => {
window.addEventListener("resize", measure);
return () => {
window.removeEventListener("resize", measure);
};
}, [measure]);
useEffect(() => {
if (!continuousPolling) return;
let aborted = false;
const cb = () => {
measure();
if (!aborted) window.requestAnimationFrame(cb);
};
window.requestAnimationFrame(cb);
return () => {
aborted = true;
};
}, [continuousPolling, measure]);
return { measure, ...dimensions.current };
}
useDimensions.windowRef = {
current: {
get clientHeight() {
return window.innerHeight;
},
get clientWidth() {
return window.innerWidth;
},
getBoundingClientRect() {
return {
x: 0,
y: 0,
};
},
},
};
useDimensions.ContinuousPollingProvider = (props: {
isEnabled?: boolean;
children?: ReactNode;
}) =>
createElement(ContinuousPollingContext.Provider, {
children: props.children,
value: props.isEnabled ?? true,
}) as JSX.Element;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment