I am using the following hook in order to get the width and the height for an element:
import { useCallback, useLayoutEffect, useState } from "react";
interface Size {
width: number;
height: number;
}
function useElementSize<T extends HTMLElement = HTMLDivElement>(): [
(node: T | null) => void,
Size,
] {
function debounce(func: Function) {
let timer: any;
return function (event: any) {
if (timer) clearTimeout(timer);
timer = setTimeout(func, 100, event);
};
}
// Mutable values like 'ref.current' aren't valid dependencies
// because mutating them doesn't re-render the component.
// Instead, we use a state as a ref to be reactive.
const [ref, setRef] = useState<T | null>(null);
const [size, setSize] = useState<Size>({
width: 0,
height: 0,
});
// Prevent too many rendering using useCallback
const handleSize = useCallback(() => {
if (ref?.offsetWidth && ref?.offsetHeight) {
setSize({
width: ref?.offsetWidth || 0,
height: ref?.offsetHeight || 0,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useLayoutEffect(() => {
window.addEventListener(
"resize",
debounce(function () {
handleSize();
}),
);
handleSize();
return () => window.removeEventListener("resize", handleSize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref?.offsetHeight, ref?.offsetWidth]);
return [setRef, size];
}
export default useElementSize;
It works perfectly - when I resize the window I see that the component re-renders every time I resize and it happens very fast.
The problem is I have a very big element and I would like to add a debounce to the useElementHook. I tried to add a debounce of 2 seconds but I get the following behavior:
This is what I tried:
import { useCallback, useLayoutEffect, useState } from "react";
interface Size {
width: number;
height: number;
}
function useElementSize<T extends HTMLElement = HTMLDivElement>(): [
(node: T | null) => void,
Size,
] {
function debounce(func: Function) {
let timer: any;
return function (event: any) {
if (timer) clearTimeout(timer);
timer = setTimeout(func, 2000, event);
};
}
// Mutable values like 'ref.current' aren't valid dependencies
// because mutating them doesn't re-render the component.
// Instead, we use a state as a ref to be reactive.
const [ref, setRef] = useState<T | null>(null);
const [size, setSize] = useState<Size>({
width: 0,
height: 0,
});
// Prevent too many rendering using useCallback
const handleSize = useCallback(() => {
if (ref?.offsetWidth && ref?.offsetHeight) {
setSize({
width: ref?.offsetWidth || 0,
height: ref?.offsetHeight || 0,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useLayoutEffect(() => {
window.addEventListener(
"resize",
debounce(function () {
handleSize();
}),
);
handleSize();
return () => window.removeEventListener("resize", handleSize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref?.offsetHeight, ref?.offsetWidth]);
return [setRef, size];
}
export default useElementSize;
Current behavior: Resize event -> doesn't do anything -> 2 seconds have passed -> I see 40 console.logs (the component still re-renders 40 times instead of once).
What I want: Resize event -> doesn't do anything -> 2 seconds have passed -> 1 console.log and re-render the component once!
Is there any solution for this?