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?
Advertisement
Answer
I’ve made some changes to your implementation to make it work: Notable changes:
- move
handleSize
inside the effect (no need foruseCallback
- use a ref to the node to avoid a dependency to the effect
import { useLayoutEffect, useRef, useState } from "react";
interface Size {
width: number;
height: number;
}
function debounce(func: Function) {
let timer: any;
return function (event: any) {
if (timer) clearTimeout(timer);
timer = setTimeout(func, 2000, event);
};
}
function useElementSize<T extends HTMLElement = HTMLDivElement>(): [
(node: T | null) => void,
Size
] {
const [node, setNode] = useState<T | null>(null);
const nodeRef = useRef(node);
const [size, setSize] = useState<Size>({
width: 0,
height: 0
});
useLayoutEffect(() => {
nodeRef.current = node;
}, [node]);
useLayoutEffect(() => {
const handleSize = () => {
if (nodeRef.current?.offsetWidth && nodeRef.current?.offsetHeight) {
setSize({
width: nodeRef.current?.offsetWidth || 0,
height: nodeRef.current?.offsetHeight || 0
});
}
};
const debouncedHandler = debounce(handleSize);
window.addEventListener("resize", debouncedHandler);
handleSize();
return () => window.removeEventListener("resize", debouncedHandler);
}, [node]);
return [setNode, size];
}
export default useElementSize;