如何向 useElementSize 挂钩添加去抖动?

How to add debounce to useElementSize hook?

我正在使用以下钩子来获取元素的宽度和高度:

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;

它完美地工作 - 当我调整 window 的大小时,我发现每次我调整大小时组件都会重新呈现,而且它发生得非常快。

问题是我有一个非常大的元素,我想为 useElementHook 添加去抖动。我尝试添加 2 秒的去抖动,但出现以下行为:

这是我试过的:

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;

当前行为: 调整大小事件 -> 什么都不做 -> 2 秒过去了 -> 我看到 40 console.logs(组件仍然重新渲染 40 次而不是一次)。

我想要的: 调整大小事件 -> 什么都不做 -> 2 秒过去了 -> 1 console.log 并重新渲染组件一次!

有解决办法吗?

我已经对您的实现进行了一些更改以使其正常工作: 显着变化:

  • handleSize 移到效果内(不需要 useCallback
  • 使用对节点的引用以避免对效果的依赖
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;