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;