useResizeObserver
Hook to handle resize events of an element.
Example
import { useRef } from "react";
import useResizeObserver from "@/hooks/useResizeObserver";
export default function UseResizeObserverExample() {
const ref = useRef<HTMLTextAreaElement>(null);
const { width, height } = useResizeObserver({
ref,
box: "border-box",
// onResize: (size) => console.log(size),
});
return (
<div className="flex resize flex-col items-center gap-4 border border-ctp-base text-ctp-text">
<p>Width: {width}</p>
<p>Height: {height}</p>
<textarea
ref={ref}
className="resize rounded bg-ctp-surface1 p-2"
rows={10}
cols={50}
defaultValue="Resize me!"
/>
</div>
);
}
Code
import { useCallback, useEffect, useRef, useState } from "react";
import type { RefObject } from "react";
type Size = {
width: number | undefined;
height: number | undefined;
};
type UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {
ref: RefObject<T>;
onResize?: (size: Size) => void;
box?: "border-box" | "content-box" | "device-pixel-content-box";
};
const initialSize: Size = {
width: undefined,
height: undefined,
};
function useIsMounted(): () => boolean {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return useCallback(() => isMounted.current, []);
}
/**
* Hook to handle resize events of an element.
* @param {UseResizeObserverOptions<T>} options - The options for the hook.
* @returns {Size} The size of the element.
*/
export default function useResizeObserver<T extends HTMLElement = HTMLElement>(
options: UseResizeObserverOptions<T>,
): Size {
const { ref, box = "content-box" } = options;
const [{ width, height }, setSize] = useState<Size>(initialSize);
const isMounted = useIsMounted();
const previousSize = useRef<Size>({ ...initialSize });
const onResize = useRef<((size: Size) => void) | undefined>(undefined);
onResize.current = options.onResize;
useEffect(() => {
if (!ref.current) return;
if (typeof window === "undefined" || !("ResizeObserver" in window)) return;
const observer = new ResizeObserver(([entry]) => {
const boxProp =
box === "border-box"
? "borderBoxSize"
: box === "device-pixel-content-box"
? "devicePixelContentBoxSize"
: "contentBoxSize";
const newWidth = extractSize(entry, boxProp, "inlineSize");
const newHeight = extractSize(entry, boxProp, "blockSize");
const hasChanged =
previousSize.current.width !== newWidth ||
previousSize.current.height !== newHeight;
if (hasChanged) {
const newSize: Size = { width: newWidth, height: newHeight };
previousSize.current.width = newWidth;
previousSize.current.height = newHeight;
if (onResize.current) {
onResize.current(newSize);
} else {
if (isMounted()) {
setSize(newSize);
}
}
}
});
observer.observe(ref.current, { box });
return () => {
observer.disconnect();
};
}, [box, ref, isMounted]);
return { width, height };
}
type BoxSizesKey = keyof Pick<
ResizeObserverEntry,
"borderBoxSize" | "contentBoxSize" | "devicePixelContentBoxSize"
>;
function extractSize(
entry: ResizeObserverEntry,
box: BoxSizesKey,
sizeType: keyof ResizeObserverSize,
): number | undefined {
if (!entry[box]) {
if (box === "contentBoxSize") {
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"];
}
return undefined;
}
return Array.isArray(entry[box])
? entry[box][0][sizeType]
: // @ts-ignore Support Firefox's non-standard behavior
(entry[box][sizeType] as number);
}