Как использовать дросселирование или устранение дребезга с помощью React Hook?

Я пытаюсь использовать метод throttle из lodash в функциональном компоненте, например:

const App = () => {
  const [value, setValue] = useState(0)
  useEffect(throttle(() => console.info(value), 1000), [value])
  return (
    <button onClick = {() => setValue(value + 1)}>{value}</button>
  )
}

Поскольку метод внутри useEffect переопределяется при каждом рендеринге, эффект дросселирования не работает.

У кого-нибудь есть простое решение?

Можно ли определить функцию дросселирования вне компонента App и просто вызвать ее в функции useEffect?

Tholle 13.02.2019 10:16

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

Alexandre Annic 13.02.2019 10:23
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
102
2
124 524
23
Перейти к ответу Данный вопрос помечен как решенный

Ответы 23

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

По прошествии некоторого времени я уверен, что гораздо проще справляться с вещами самостоятельно с помощью setTimeout/clearTimeout (и перемещая это в отдельный пользовательский хук), чем работать с функциональными помощниками. Обработка позже одного создает дополнительные проблемы сразу после того, как мы применим это к useCallback, который может быть воссоздан из-за изменения зависимости, но мы не хотим сбрасывать отложенное выполнение.

оригинальный ответ ниже

вам может (и, вероятно, понадобится) useRef для хранения значения между рендерами. Так же, как это рекомендуется для таймеров

Что-то такое

const App = () => {
  const [value, setValue] = useState(0)
  const throttled = useRef(throttle((newValue) => console.info(newValue), 1000))

  useEffect(() => throttled.current(value), [value])

  return (
    <button onClick = {() => setValue(value + 1)}>{value}</button>
  )
}

Что касается useCallback:

Это может работать также как

const throttled = useCallback(throttle(newValue => console.info(newValue), 1000), []);

Но если мы попытаемся воссоздать обратный вызов после изменения value:

const throttled = useCallback(throttle(() => console.info(value), 1000), [value]);

мы можем обнаружить, что это не задерживает выполнение: как только value изменяется, обратный вызов немедленно воссоздается и выполняется.

Так что я вижу useCallback в случае отложенного запуска не дает существенного преимущества. Тебе решать.

[UPD] изначально было

  const throttled = useRef(throttle(() => console.info(value), 1000))

  useEffect(throttled.current, [value])

но таким образом throttled.current связался с начальным value(0) замыканием. Поэтому он никогда не менялся даже на следующих рендерах.

Так что будьте осторожны, помещая функции в useRef из-за функции закрытия.

useRef должен быть ответом, но этот код не работает. Я пытаюсь это исправить.
Alexandre Annic 13.02.2019 10:30

возможно, я пропустил начальное значение этой части, поскольку useRef делает закрытие до начального значения

skyboyer 13.02.2019 10:30

Throttle-debounce сначала использует DELAY, затем CALLBACK;)

mikes 11.09.2019 22:05

@mikes это зависит (для версии lodash есть опции leading и trailing для настройки github.com/lodash/lodash/blob/master/throttle.js)

skyboyer 11.09.2019 22:20

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

hossein alipour 06.12.2019 12:24

@hossein alipur не может не согласиться с вами. после того, как прошел почти год с тех пор, как я ответил, теперь я вижу, что лучше контролировать вещи непосредственно внутри useEffect с помощью setTimeout/clearTimeout, чем использовать вспомогательную функцию debounce. Спасибо, что подняли это.

skyboyer 07.12.2019 11:50

Мы можем использовать useRef для создания обратного вызова и его сохранения, но я считаю, что лучше использовать useCallback даже для передачи необходимых переменных, что случается редко. Мы можем использовать setValue, чтобы изменить значение внутри useCallback, не добавляя value в массив зависимостей, и даже получить доступ к предыдущему значению, используя setValue(previous => ...). Если нам нужен прямой доступ к значению без его изменения, мы можем передать его в качестве аргумента, как вы делаете с useRef в своем примере, например useCallback(throttle((value) => { ... }, 1000), []).

Christos Lytras 27.05.2020 11:48

Спасибо @ChristosLytras. Я дал ответ ниже, как реализовать его с помощью хука useCallback.

Jaskaran Singh 11.12.2020 09:06

Итак, какая часть этого ответа является фактическим ответом? Это немного извилисто.

coler-j 01.04.2021 14:47

Этот ответ настолько сбивает с толку, согласен с @coler-j

alexr89 04.08.2021 10:40

Я написал два простых хука (использовать эффект дросселирования и использовать дебаунс-эффект) для этого варианта использования, возможно, это будет полезно для кого-то еще, кто ищет простое решение.

import React, { useState } from 'react';
import useThrottledEffect  from 'use-throttled-effect';

export default function Input() {
  const [count, setCount] = useState(0);

  useEffect(()=>{
    const interval = setInterval(() => setCount(count=>count+1) ,100);
    return ()=>clearInterval(interval);
  },[])

  useThrottledEffect(()=>{
    console.info(count);     
  }, 1000 ,[count]);

  return (
    {count}
  );
}

сработало отлично, спасибо ? Протестировал оба крючка

Lukas Liesis 16.06.2021 16:43

Если вы используете его в обработчике, я совершенно уверен, что это способ сделать это.

function useThrottleScroll() {
  const savedHandler = useRef();

  function handleEvent() {}

  useEffect(() => {
    savedHandleEvent.current = handleEvent;
  }, []);

  const throttleOnScroll = useRef(throttle((event) => savedHandleEvent.current(event), 100)).current;

  function handleEventPersistence(event) {
    return throttleOnScroll(event);
  }

  return {
    onScroll: handleEventPersistence,
  };
}

Я использую что-то вроде этого, и он отлично работает:

let debouncer = debounce(
  f => f(),
  1000,
  { leading: true }, // debounce one on leading and one on trailing
);

function App(){
   let [state, setState] = useState();

   useEffect(() => debouncer(()=>{
       // you can use state here for new state value
   }),[state])

   return <div />
}

откуда debounce()?

godblessstrawberry 07.07.2020 22:44

Это может быть крошечный пользовательский хук, например:

useDebounce.js

import React, { useState, useEffect } from 'react';

export default (value, timeout) => {
    const [state, setState] = useState(value);

    useEffect(() => {
        const handler = setTimeout(() => setState(value), timeout);

        return () => clearTimeout(handler);
    }, [value, timeout]);

    return state;
}

Пример использования:

import React, { useEffect } from 'react';

import useDebounce from '/path/to/useDebounce';

const App = (props) => {
    const [state, setState] = useState({title: ''});    
    const debouncedTitle = useDebounce(state.title, 1000);

    useEffect(() => {
        // do whatever you want with state.title/debouncedTitle
    }, [debouncedTitle]);        

    return (
        // ...
    );
}
// ...

Примечание: Как вы, наверное, знаете, useEffect всегда запускается при начальном рендеринге, и из-за этого, если вы используете мой ответ, вы, вероятно, увидите, что рендеринг вашего компонента выполняется дважды, не волнуйтесь, вам просто нужно написать еще один пользовательский хук. проверьте мой другой ответ для получения дополнительной информации.

Я не понимаю, как избежать второго (или первого) рендера, даже используя связанный хук. Не могли бы вы привести пример? Спасибо

andreapier 19.08.2021 10:45

@andreapier Я уже добавил ссылку на другой пользовательский хук, чтобы предотвратить рендеринг при начальном рендеринге, если вы его не видели, вот ссылка: stackoverflow.com/a/57941438/3367974

Mehdi Dehghani 19.08.2021 11:17

Да, я это видел. Мой вопрос был о том, как заставить их работать вместе. Однако я переключился на другое решение, так как это (на мой взгляд) создает слишком много проблем.

andreapier 21.08.2021 18:13

Если вы имеете в виду использование useDebounce вместе с useDidMountEffect, вам просто нужно заменить useEffect на useDidMountEffect в приведенном выше примере, и все готово.

Mehdi Dehghani 21.08.2021 19:08

В моем случае мне также нужно было пройти событие. Пошел с этим:

const MyComponent = () => {
  const handleScroll = useMemo(() => {
    const throttled = throttle(e => console.info(e.target.scrollLeft), 300);
    return e => {
      e.persist();
      return throttled(e);
    };
  }, []);
  return <div onScroll = {handleScroll}>Content</div>;
};

Я создал свой собственный хук под названием useDebouncedEffect, который будет ждать выполнения useEffect до тех пор, пока состояние не обновится на время задержки.

В этом примере ваш эффект будет зарегистрирован в консоли после того, как вы перестанете нажимать кнопку в течение 1 секунды.

Пример песочницыhttps://codesandbox.io/s/react-use-debounced-effect-6jppw

App.jsx

import { useState } from "react";
import { useDebouncedEffect } from "./useDebouncedEffect";

const App = () => {
  const [value, setValue] = useState(0)

  useDebouncedEffect(() => console.info(value), [value], 1000);

  return (
    <button onClick = {() => setValue(value + 1)}>{value}</button>
  )
}

export default App;

useDebouncedEffect.js

import { useEffect } from "react";

export const useDebouncedEffect = (effect, deps, delay) => {
    useEffect(() => {
        const handler = setTimeout(() => effect(), delay);

        return () => clearTimeout(handler);
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [...deps || [], delay]);
}

Комментарий для отключения исчерпывающих отложений обязателен, если вы не хотите видеть предупреждение, потому что lint всегда будет жаловаться на отсутствие эффекта в качестве зависимости. Добавление эффекта в качестве зависимости будет запускать useEffect при каждом рендеринге. Вместо этого вы можете добавить проверку в useDebouncedEffect, чтобы убедиться, что он передает все зависимости. (увидеть ниже)

Добавление исчерпывающей проверки зависимостей в useDebouncedEffect

Если вы хотите, чтобы eslint проверял useDebouncedEffect исчерпывающие зависимости, вы можете добавить его в конфигурацию eslint в package.json

  "eslintConfig": {
    "extends": [
      "react-app"
    ],
    "rules": {
      "react-hooks/exhaustive-deps": ["warn", {
        "additionalHooks": "useDebouncedEffect"
      }]
    }
  },

https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks#advanced-configuration

Если вам интересно, зачем нужен useCallback, я думаю, причина в следующем: функции в JavaScript не имеют ссылочного равенства (то есть () => {} === () => {} // false). Поэтому каждый раз, когда компонент перерисовывается effect, он не такой, как был раньше. Однако, используя useCallback, вы говорите React: «Пожалуйста, считайте, что я изменился только тогда, когда мои deps тоже изменились!»

David 09.01.2021 11:05

Функции @David действительно имеют ссылочное равенство, поэтому вам нужно useCallback в первую очередь. Ваш пример относится к структурному равенству, а не к ссылочному равенству.

Kevin Beal 21.05.2021 21:39

@KevinBeal, я не думаю, что слышал термин «структурное равенство» раньше, и быстрый поиск в Интернете (на языке Kotlin) говорит, что ссылочное — это ===, а структурное — ==. Согласно этой логике, мне кажется, что функции имеют структурное равенство в JavaScript.

David 27.05.2021 17:09

Структурное равенство @David просто означает, что значения одинаковы внутри, с одинаковыми ключами, значениями и т. д. Это равенство значений или как бы вы это ни называли.

Kevin Beal 28.05.2021 00:08

useThrottle , useDebounce

Как использовать оба

const App = () => {
  const [value, setValue] = useState(0);
  // called at most once per second (same API with useDebounce)
  const throttledCb = useThrottle(() => console.info(value), 1000);
  // usage with useEffect: invoke throttledCb on value change
  useEffect(throttledCb, [value]);
  // usage as event handler
  <button onClick = {throttledCb}>log value</button>
  // ... other render code
};

useThrottle (Лодаш)

import _ from "lodash"

function useThrottle(cb, delay) {
  const options = { leading: true, trailing: false }; // add custom lodash options
  const cbRef = useRef(cb);
  // use mutable ref to make useCallback/throttle not depend on `cb` dep
  useEffect(() => { cbRef.current = cb; });
  return useCallback(
    _.throttle((...args) => cbRef.current(...args), delay, options),
    [delay]
  );
}

const App = () => {
  const [value, setValue] = useState(0);
  const invokeDebounced = useThrottle(
    () => console.info("changed throttled value:", value),
    1000
  );
  useEffect(invokeDebounced, [value]);
  return (
    <div>
      <button onClick = {() => setValue(value + 1)}>{value}</button>
      <p>value will be logged at most once per second.</p>
    </div>
  );
};

function useThrottle(cb, delay) {
  const options = { leading: true, trailing: false }; // pass custom lodash options
  const cbRef = useRef(cb);
  useEffect(() => {
    cbRef.current = cb;
  });
  return useCallback(
    _.throttle((...args) => cbRef.current(...args), delay, options),
    [delay]
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity = "sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ = " crossorigin = "anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id = "root"></div>

useDebounce (Лодаш)

import _ from "lodash"

function useDebounce(cb, delay) {
  // ...
  const inputsRef = useRef({cb, delay}); // mutable ref like with useThrottle
  useEffect(() => { inputsRef.current = { cb, delay }; }); //also track cur. delay
  return useCallback(
    _.debounce((...args) => {
        // Debounce is an async callback. Cancel it, if in the meanwhile
        // (1) component has been unmounted (see isMounted in snippet)
        // (2) delay has changed
        if (inputsRef.current.delay === delay && isMounted())
          inputsRef.current.cb(...args);
      }, delay, options
    ),
    [delay, _.debounce]
  );
}

const App = () => {
  const [value, setValue] = useState(0);
  const invokeDebounced = useDebounce(
    () => console.info("debounced", value),
    1000
  );
  useEffect(invokeDebounced, [value]);
  return (
    <div>
      <button onClick = {() => setValue(value + 1)}>{value}</button>
      <p> Logging is delayed until after 1 sec. has elapsed since the last invocation.</p>
    </div>
  );
};

function useDebounce(cb, delay) {
  const options = {
    leading: false,
    trailing: true
  };
  const inputsRef = useRef(cb);
  const isMounted = useIsMounted();
  useEffect(() => {
    inputsRef.current = { cb, delay };
  });

  return useCallback(
    _.debounce(
      (...args) => {
        // Don't execute callback, if (1) component in the meanwhile 
        // has been unmounted or (2) delay has changed
        if (inputsRef.current.delay === delay && isMounted())
          inputsRef.current.cb(...args);
      },
      delay,
      options
    ),
    [delay, _.debounce]
  );
}

function useIsMounted() {
  const isMountedRef = useRef(true);
  useEffect(() => {
    return () => {
      isMountedRef.current = false;
    };
  }, []);
  return () => isMountedRef.current;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity = "sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ = " crossorigin = "anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id = "root"></div>

Настройки

1. Вы можете заменить Lodash своим собственным кодом throttle или debounce, например:

const debounceImpl = (cb, delay) => {
  let isDebounced = null;
  return (...args) => {
    clearTimeout(isDebounced);
    isDebounced = setTimeout(() => cb(...args), delay);
  };
};

const throttleImpl = (cb, delay) => {
  let isThrottled = false;
  return (...args) => {
    if (isThrottled) return;
    isThrottled = true;
    cb(...args);
    setTimeout(() => {
      isThrottled = false;
    }, delay);
  };
};

const App = () => {
  const [value, setValue] = useState(0);
  const invokeThrottled = useThrottle(
    () => console.info("throttled", value),
    1000
  );
  const invokeDebounced = useDebounce(
    () => console.info("debounced", value),
    1000
  );
  useEffect(invokeThrottled, [value]);
  useEffect(invokeDebounced, [value]);
  return <button onClick = {() => setValue(value + 1)}>{value}</button>;
};

function useThrottle(cb, delay) {
  const cbRef = useRef(cb);
  useEffect(() => {
    cbRef.current = cb;
  });
  return useCallback(
    throttleImpl((...args) => cbRef.current(...args), delay),
    [delay]
  );
}

function useDebounce(cb, delay) {
  const cbRef = useRef(cb);
  useEffect(() => {
    cbRef.current = cb;
  });
  return useCallback(
    debounceImpl((...args) => cbRef.current(...args), delay),
    [delay]
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id = "root"></div>

2. useThrottle можно сократить, если всегда использовать с useEffect (то же самое для useDebounce):

const App = () => {
  // useEffect now is contained inside useThrottle
  useThrottle(() => console.info(value), 1000, [value]);
  // ...
};

const App = () => {
  const [value, setValue] = useState(0);
  useThrottle(() => console.info(value), 1000, [value]);
  return (
    <div>
      <button onClick = {() => setValue(value + 1)}>{value}</button>
      <p>value will be logged at most once per second.</p>
    </div>
  );
};

function useThrottle(cb, delay, additionalDeps) {
  const options = { leading: true, trailing: false }; // pass custom lodash options
  const cbRef = useRef(cb);
  const throttledCb = useCallback(
    _.throttle((...args) => cbRef.current(...args), delay, options),
    [delay]
  );
  useEffect(() => {
    cbRef.current = cb;
  });
  // set additionalDeps to execute effect, when other values change (not only on delay change)
  useEffect(throttledCb, [throttledCb, ...additionalDeps]);
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity = "sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ = " crossorigin = "anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id = "root"></div>

Зачем использовать useEffect(() => { cbRef.current = cb; }); без какой-либо зависимости? Это означает, что мы запускаем эффект при каждом повторном рендеринге, так почему бы просто не назначить без useEffect?

ogostos 14.07.2020 08:25

Хороший вопрос — он предназначен для того, чтобы всегда содержать самый последний обратный вызов внутри cbRef. Изменяемую ссылку можно использовать как переменная экземпляра для хуков — здесь является примером с setInterval из блога Overreacted. Фаза рендеринга также должна быть чистой без побочных эффектов, например. для совместимости с параллельным режимом React. Вот почему мы заключаем задание внутрь useEffect.

ford04 14.07.2020 09:55

Кажется, я получаю сообщение об ошибке при использовании useThrottle (Lodash): «TypeError: Cannot read property 'apply' of undefined». В сочетании с этим у меня есть ошибка ESLint, говорящая: «React Hook useCallback получил функцию, зависимости которой неизвестны. Вместо этого передайте встроенную функцию».

alexr89 05.08.2021 21:52

Используя функцию debounce lodash, вот что я делаю:

import debounce from 'lodash/debounce'

// The function that we want to debounce, for example the function that makes the API calls
const getUsers = (event) => {
// ...
}


// The magic!
const debouncedGetUsers = useCallback(debounce(getUsers, 500), [])

В вашем JSX:

<input value = {value} onChange = {debouncedGetUsers} />

Я довольно поздно с этим, но вот способ опровергнуть setState()

/**
 * Like React.setState, but debounces the setter.
 * 
 * @param {*} initialValue - The initial value for setState().
 * @param {int} delay - The debounce delay, in milliseconds.
 */
export const useDebouncedState = (initialValue, delay) => {
  const [val, setVal] = React.useState(initialValue);
  const timeout = React.useRef();
  const debouncedSetVal = newVal => {
    timeout.current && clearTimeout(timeout.current);
    timeout.current = setTimeout(() => setVal(newVal), delay);
  };

  React.useEffect(() => () => clearTimeout(timeout.current), []);
  return [val, debouncedSetVal];
};

Это мой useDebounce:

export function useDebounce(callback, timeout, deps) {
    const timeoutId = useRef();

    useEffect(() => {
        clearTimeout(timeoutId.current);
        timeoutId.current = setTimeout(callback, timeout);

        return () => clearTimeout(timeoutId.current);
    }, deps);
}

И вы можете использовать его следующим образом:

const TIMEOUT = 500; // wait 500 milliseconds;

export function AppContainer(props) {
    const { dataId } = props;
    const [data, setData] = useState(null);
    //
    useDebounce(
        async () => {
            data = await loadDataFromAPI(dataId);
            setData(data);
        }, 
        TIMEOUT, 
        [dataId]
    );
    //
}

Я пишу простой useDebounce хук, который учитывает очистку, как работает useEffect.

import { useState, useEffect, useRef, useCallback } from "react";

export function useDebounceState<T>(initValue: T, delay: number) {
  const [value, setValue] = useState<T>(initValue);
  const timerRef = useRef(null);
  // reset timer when delay changes
  useEffect(
    function () {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    },
    [delay]
  );
  const debounceSetValue = useCallback(
    function (val) {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
      timerRef.current = setTimeout(function () {
        setValue(val);
      }, delay);
    },
    [delay]
  );
  return [value, debounceSetValue];
}

interface DebounceOptions {
  imediate?: boolean;
  initArgs?: any[];
}

const INIT_VALUE = -1;
export function useDebounce(fn, delay: number, options: DebounceOptions = {}) {
  const [num, setNum] = useDebounceState(INIT_VALUE, delay);
  // save actual arguments when fn called
  const callArgRef = useRef(options.initArgs || []);
  // save real callback function
  const fnRef = useRef(fn);
  // wrapped function
  const trigger = useCallback(function () {
    callArgRef.current = [].slice.call(arguments);
    setNum((prev) => {
      return prev + 1;
    });
  }, []);
  // update real callback
  useEffect(function () {
    fnRef.current = fn;
  });
  useEffect(
    function () {
      if (num === INIT_VALUE && !options.imediate) {
        // prevent init call
        return;
      }
      return fnRef.current.apply(null, callArgRef.current);
    },
    [num, options.imediate]
  );
  return trigger;
}

суть здесь: https://gist.github.com/sophister/9cc74bb7f0509bdd6e763edbbd21ba64

а это живая демонстрация: https://codesandbox.io/s/react-hook-debounce-demo-mgr89?file=/src/App.js

использование:

const debounceChange = useDebounce(function (e) {
    console.info("debounced text change: " + e.target.value);
  }, 500);
  // can't use debounceChange directly, since react using event pooling
  function deboucnedCallback(e) {
    e.persist();
    debounceChange(e);
  }

// later the jsx
<input onChange = {deboucnedCallback} />

Я хотел бы присоединиться к вечеринке со своим заблокированным и отклоненным вводом, используя useState:

// import { useState, useRef } from 'react' // nomral import
const { useState, useRef } = React // inline import

// Throttle

const ThrottledInput = ({ onChange, delay = 500 }) => {
  const t = useRef()
  
  const handleChangeEvent = ({ target }) => {
    if (!t.current) {
      t.current = setTimeout(() => {
        onChange(target.value)
        clearTimeout(t)
        t.current = null
      }, delay)
    }
  }
  
  return (
    <input
      placeholder = "throttle"
      onChange = {handleChangeEvent}
    />
  )
}


// Debounce

const DebouncedInput = ({ onChange, delay = 500 }) => {
  const t = useRef()
  
  const handleChangeEvent = ({ target }) => {
    clearTimeout(t.current)
    t.current = setTimeout(() => onChange(target.value), delay)
  }
  
  return (
    <input
      placeholder = "debounce"
      onChange = {handleChangeEvent}
    />
  )
}

// ----

ReactDOM.render(<div>
  <ThrottledInput onChange = {console.info} />
  <DebouncedInput onChange = {console.info} />
</div>, document.getElementById('root'))
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id = "root"></div>

Вот настоящий дроссельный крюк. Вы можете использовать экран или компонент для всех функций, которые хотите регулировать, и они будут использовать одно и то же регулирование. Или вы можете вызывать useThrottle() несколько раз и иметь разные дроссели для отдельных функций.

Используйте так:

import useThrottle from '../hooks/useThrottle';

const [navigateToSignIn, navigateToCreateAccount] = useThrottle([
        () => { navigation.navigate(NavigationRouteNames.SignIn) },
        () => { navigation.navigate(NavigationRouteNames.CreateAccount) }
    ])

И сам крючок:

import { useCallback, useState } from "react";

// Throttles all callbacks on a component within the same throttle.  
// All callbacks passed in will share the same throttle.

const THROTTLE_DURATION = 500;

export default (callbacks: Array<() => any>) => {
    const [isWaiting, setIsWaiting] = useState(false);

    const throttledCallbacks = callbacks.map((callback) => {
        return useCallback(() => {
            if (!isWaiting) {
                callback()
                setIsWaiting(true)
                setTimeout(() => {
                    setIsWaiting(false)
                }, THROTTLE_DURATION);
            }
        }, [isWaiting]);
    })

    return throttledCallbacks;
}

Вот простой хук, чтобы отклонить ваши звонки.

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

const { debounceRequest } = useDebounce(someFn);

И тогда назовите это так

debounceRequest(); 

Реализация показана ниже

import React from "react";

const useDebounce = (callbackFn: () => any, timeout: number = 500) => {
const [sends, setSends] = React.useState(0);

const stabilizedCallbackFn = React.useCallback(callbackFn, [callbackFn]);

const debounceRequest = () => {
  setSends(sends + 1);
};

// 1st send, 2nd send, 3rd send, 4th send ...
// when the 2nd send comes, then 1st set timeout is cancelled via clearInterval
// when the 3rd send comes, then 2nd set timeout is cancelled via clearInterval
// process continues till timeout has passed, then stabilizedCallbackFn gets called
// return () => clearInterval(id) is critical operation since _this_ is what cancels 
//  the previous send.
// *? return () => clearInterval(id) is called for the previous send when a new send 
// is sent. Essentially, within the timeout all but the last send gets called.

React.useEffect(() => {
  if (sends > 0) {
     const id = window.setTimeout(() => {
       stabilizedCallbackFn();
       setSends(0);
     }, timeout);
     return () => {
      return window.clearInterval(id);
     };
  }
 }, [stabilizedCallbackFn, sends, timeout]);

 return {
   debounceRequest,
 };
};

export default useDebounce;
const useDebounce = (func: any) => {
    const debounceFunc = useRef(null);

    useEffect(() => {
        if (func) {
            // @ts-ignore
            debounceFunc.current = debounce(func, 1000);
        }
    }, []);

    const debFunc = () => {
        if (debounceFunc.current) {
            return debounceFunc.current;
        }
        return func;
    };
    return debFunc();
};

Дебунс с помощью хука useCallback.

import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';

function App() {
    const [value, setValue] = useState('');
    const [dbValue, saveToDb] = useState(''); // would be an API call normally

    // highlight-starts
    const debouncedSave = useCallback(
        debounce(nextValue => saveToDb(nextValue), 1000),
        [], // will be created only once initially
    );
    // highlight-ends

    const handleChangeEvent = event => {
        const { value: nextValue } = event.target;
        setValue(nextValue);
        // Even though handleChangeEvent is created on each render and executed
        // it references the same debouncedSave that was created initially
        debouncedSave(nextValue);
    };

    return <div></div>;
}

react-table имеет хорошую useAsyncDebounce функцию, представленную на https://react-table.tanstack.com/docs/faq#how-can-i-debounce-rapid-table-state-changes

И еще одна реализация. Пользовательский крючок:

function useThrottle (func, delay) {
  const [timeout, saveTimeout] = useState(null);

  const throttledFunc = function () {
    if (timeout) {
      clearTimeout(timeout);
    }

    const newTimeout = setTimeout(() => {
      func(...arguments);
      if (newTimeout === timeout) {
        saveTimeout(null);
      }
    }, delay);

    saveTimeout(newTimeout);
  }

  return throttledFunc;
}

и использование:

const throttledFunc = useThrottle(someFunc, 200);

Надеюсь, это поможет кому-то.

Я сделал простой хук для создания экземпляров дросселя.

Он использует немного другой подход, передавая функцию для вызова каждый раз, а не пытаясь обернуть ее и управлять мутациями. Многие другие решения не учитывают, что функция может вызвать потенциальное изменение. Паттерн хорошо работает с газом или дебаунсом.

// useThrottle.js
import React, { useCallback } from 'react';
import throttle from 'lodash/throttle';

export function useThrottle(timeout = 300, opts = {}) {
  return useCallback(throttle((fn, ...args) => {
    fn(...args);
  }, timeout, opts), [timeout]);
}

Пример использования:

...
const throttleX = useThrottle(100);

const updateX = useCallback((event) => {
  // do something!
}, [someMutableValue])

return ( 
 <div onPointerMove = {(event) => throttleX(updateX, event)}></div>
)
...

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

import { useState, useRef, useEffect } from 'react';

const useDebounce = <T>(
  value: T,
  timeout: number,
  immediate: boolean = true
): T => {
  const [state, setState] = useState<T>(value);
  const handler = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

  useEffect(() => {
    if (handler.current) {
      clearTimeout(handler.current);
      handler.current = undefined;
    } else if (immediate) {
      setState(value);
    }

    handler.current = setTimeout(() => {
      setState(value);
      handler.current = undefined;
    }, timeout);
  }, [value, timeout, immediate]);

  return state;
};

export default useDebounce;

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

Мы можем сохранить отмененную функцию в ref и обновлять ее каждый раз, когда компонент перерисовывается в useEffect следующим образом:

  // some state
  const [counter, setCounter] = useState(0);

  // store a ref to the function we will debounce
  const increment = useRef(null);

  // update the ref every time the component rerenders
  useEffect(() => {
    increment.current = () => {
      setCounter(counter + 1);
    };
  });

  // debounce callback, which we can call (i.e. in button.onClick)
  const debouncedIncrement = useCallback(
    debounce(() => {
      if (increment) {
        increment.current();
      }
    }, 1500),
    []
  );

  // cancel active debounces on component unmount
  useEffect(() => {
    return () => {
      debouncedIncrement.cancel();
    };
  }, []);

Песочница кода: https://codesandbox.io/s/debounced-function-ref-pdrfu?file=/src/index.js

Я надеюсь, что это сэкономит кому-то несколько часов борьбы

Вы можете использовать хук useMemo для оптимизации обработчика регулируемых событий.

Пример кода ниже:

const App = () => {
  const [value, setValue] = useState(0);

  // ORIGINAL EVENT HANDLER
  function eventHandler(event) {
    setValue(value + 1);
  }

  // THROTTLED EVENT HANDLER
  const throttledEventHandler = useMemo(() => throttle(eventHandler, 1000), [value]);
  
  return (
    <button onClick = {throttledEventHandler}>Throttled Button with value: {value}</button>
  )
}

Эта памятка обновляет состояние, это нормально? Меня интересует эта инструкция от React: «Помните, что функция, переданная в useMemo, запускается во время рендеринга. Не делайте там ничего, что вы обычно не делаете во время рендеринга. Например, побочные эффекты относятся к useEffect, а не к useMemo».

user2078023 02.12.2021 10:16

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