Как добавить debounce в хук useElementSize?

Я использую следующий хук, чтобы получить ширину и высоту элемента:

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;

Это работает отлично - когда я изменяю размер окна, я вижу, что компонент перерисовывается каждый раз, когда я изменяю размер, и это происходит очень быстро.

Проблема в том, что у меня очень большой элемент, и я хотел бы добавить откат к 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 и один раз перерендерить компонент!

Есть ли какое-нибудь решение для этого?

Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Сравнение структур данных: Массивы и объекты в Javascript
Сравнение структур данных: Массивы и объекты в Javascript
Итак, вы изучили основы JavaScript и хотите перейти к изучению структур данных. Мотивация для изучения/понимания Структур данных может быть разной,...
Создание собственной системы электронной коммерции на базе Keystone.js - настройка среды и базовые модели
Создание собственной системы электронной коммерции на базе Keystone.js - настройка среды и базовые модели
Прошлая статья была первой из цикла статей о создании системы электронной коммерции с использованием Keystone.js, и она была посвящена главным образом...
Приложение для отслеживания бюджета на React js для начинающих
Приложение для отслеживания бюджета на React js для начинающих
Обучение на практике - это проверенная тема для достижения успеха в любой области. Если вы знаете контекст фразы "Практика делает человека...
Стоит ли использовать React в 2022 году?
Стоит ли использовать React в 2022 году?
В 2022 году мы все слышим о трендах фронтенда (React, Vue), но мы не знаем, почему мы должны использовать эти фреймворки, когда их использовать, а...
1
0
44
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я внес некоторые изменения в вашу реализацию, чтобы заставить ее работать: Заметные изменения:

  • переместите 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;

Edit nervous-surf-ei3wmx

Спасибо! очень ценю ваши усилия, но я не получаю размер при первом рендеринге, как я могу заставить его работать и при первом рендеринге?

Or Nakash 17.05.2022 12:59

Вы король. только что добавил setSize к первому useLayoutEffect. Спасибо!

Or Nakash 17.05.2022 13:09

просто добавьте node в список зависимостей второго useLayoutEffect (обновил ответ)

thedude 17.05.2022 13:17

Другие вопросы по теме