Skip to content
Advertisement

How to add debounce to useElementSize hook?

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 for useCallback
  • 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;

Edit nervous-surf-ei3wmx

User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement