如何通过 React Hook 使用节流阀或去抖动?

Posted

技术标签:

【中文标题】如何通过 React Hook 使用节流阀或去抖动?【英文标题】:How to use throttle or debounce with React Hook? 【发布时间】:2019-07-07 01:30:53 【问题描述】:

我正在尝试在功能组件中使用来自lodashthrottle 方法,例如:

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

由于useEffect里面的方法在每次渲染时都会重新声明,所以节流效果不起作用。

有人有简单的解决方案吗?

【问题讨论】:

您是否可以选择在App 组件之外定义节流函数并在useEffect 函数中调用它? 是的,我试过了,它可以工作,但就我而言,它不是很优雅,因为我在节流方法中使用了组件变量。 【参考方案1】:

我想使用useState 使用我的节流和去抖动输入加入聚会:

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

// Throttle

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



// Debounce

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


// ----

ReactDOM.render(<div>
  <ThrottledInput onChange=console.log />
  <DebouncedInput onChange=console.log />
</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>

【讨论】:

【参考方案2】:

您可以使用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。” 【参考方案3】:

useThrottle , useDebounce

如何同时使用

const App = () => 
  const [value, setValue] = useState(0);
  // called at most once per second (same API with useDebounce)
  const throttledCb = useThrottle(() => console.log(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 (Lodash)

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.log("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 (Lodash)

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.log("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 替换为您自己的 throttledebounce 代码,例如:

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.log("throttled", value),
    1000
  );
  const invokeDebounced = useDebounce(
    () => console.log("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.log(value), 1000, [value]);
  // ...
;

const App = () => 
  const [value, setValue] = useState(0);
  useThrottle(() => console.log(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(() =&gt; cbRef.current = cb; ); 没有任何依赖?这意味着我们在每次重新渲染时运行效果,那么为什么不简单地分配而不使用 useEffect 呢? 好问题 - 这旨在始终包含 cbRef 内的最新回调。可变 ref 可以像 instance variable 一样用于 Hooks - here 是 Overreacted 博客中 setInterval 的一个示例。渲染阶段也应该是纯的,没有副作用,例如兼容 React 并发模式。这就是为什么我们将作业包装在 useEffect 中。 我似乎在使用 useThrottle (Lodash) 时遇到错误:“TypeError: Cannot read property 'apply' of undefined”。再加上,我有一个 ESLint 错误,说“React Hook useCallback 收到了一个依赖项未知的函数。改为传递一个内联函数。”【参考方案4】:

在尝试解决陈旧状态问题时,我刚刚想到了以下模式:

我们可以将 debounced 函数存储在一个 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

我希望这可以为人们节省几个小时的挣扎

【讨论】:

【参考方案5】:

我创建了自己的自定义挂钩,称为 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.log(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 的详尽依赖关系,你可以将它添加到 package.json 的 eslint 配置中

  "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 中的函数不具有引用相等性(即() =&gt; === () =&gt; // false)。所以每次组件重新渲染时effect 都和以前不一样了。然而,使用 useCallback 你是在告诉 React '请仅在我的 deps 也发生变化时才考虑我的变化!' @David Functions 绝对具有引用相等性,这就是您首先需要useCallback 的原因。您的示例是结构平等,而不是引用平等。 @KevinBeal,我想我以前没有听说过结构平等这个词,并且快速的互联网搜索(在 Kotlin 中)说引用是 === 和结构是 ==。根据这个逻辑,在我看来,函数在 JavaScript 中具有结构相等性 @David 结构上的平等只是意味着内部的值是相同的,具有相同的键、值等。它是价值平等或其他任何你称之为的东西。【参考方案6】:

我相信这个钩子可以通过提供立即触发的选项正常工作。

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;

【讨论】:

【参考方案7】:

我做了一个简单的钩子来创建油门实例。

它采用了一种稍微不同的方法,每次都传入要调用的函数,而不是尝试包装它并管理突变。许多其他解决方案没有考虑到调用可能发生变化的函数。模式适用于油门或去抖动。

// 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>
)
...

【讨论】:

【参考方案8】:

还有一个实现。自定义钩子:

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);

希望对某人有所帮助。

【讨论】:

【参考方案9】:

react-table 有一个很好的 useAsyncDebounce 函数,位于 https://react-table.tanstack.com/docs/faq#how-can-i-debounce-rapid-table-state-changes

【讨论】:

【参考方案10】:

在 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 handleChange = event => 
        const  value: nextValue  = event.target;
        setValue(nextValue);
        // Even though handleChange is created on each render and executed
        // it references the same debouncedSave that was created initially
        debouncedSave(nextValue);
    ;

    return <div></div>;

【讨论】:

【参考方案11】:
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();
;

【讨论】:

【参考方案12】:

这里有一个简单的钩子来消除你的调用。

要使用下面的代码,你所要做的就是声明它

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;

【讨论】:

【参考方案13】:

这是一个实际的油门挂钩。您可以在屏幕或组件中使用您想要限制的所有功能,并且它们将共享相同的限制。或者您可以多次调用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;

【讨论】:

【参考方案14】:

我写了一个简单的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.log("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 />

【讨论】:

【参考方案15】:

这是我的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]
    );
    //

【讨论】:

【参考方案16】:

我已经很晚了,但这是一种去抖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];
;

【讨论】:

【参考方案17】:

我在这里使用 lodash 的 debounce 功能:

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 />

【讨论】:

【参考方案18】:

它可能是一个很小的自定义钩子,像这样:

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 总是在初始渲染时运行,因此如果您使用我的回答,您可能会看到组件的渲染运行两次,不用担心,您只需要编写另一个自定义钩子。查看my other answer了解更多信息。

【讨论】:

我不明白如何避免第二次(或第一次)渲染,即使使用链接的钩子。你能举个例子吗?谢谢 @andreapier 我已经添加了指向另一个自定义钩子的链接,以防止在初始渲染时进行渲染,因为您没有看到它,这里是链接:***.com/a/57941438/3367974 是的,我看到了。我的问题是关于如何让两者一起工作。但是,我改用另一种解决方案,因为这种解决方案(在我看来)存在太多问题。 如果您的意思是将useDebounceuseDidMountEffect 一起使用,则只需在上面的示例中将useEffect 替换为useDidMountEffect 即可。【参考方案19】:

在我的情况下,我还需要传递事件。和这个一起去:

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

【讨论】:

【参考方案20】:

我用这样的东西,效果很好:

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() 来自哪里?【参考方案21】:

经过一段时间后,我确信使用setTimeout/clearTimeout(并将其移动到单独的自定义挂钩中)自己处理事情比使用函数式助手要容易得多。在我们将其应用到 useCallback 之后,处理稍后会产生额外的挑战,这些挑战可以由于依赖关系更改而重新创建,但我们不想重置延迟运行。

下面的原始答案

您可能(并且可能需要)useRef 在渲染之间存储值。就像suggested for timers

类似的东西

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

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

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

至于useCallback

它也可以作为

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

但是如果我们尝试在 value 更改后重新创建回调:

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

我们可能会发现它不会延迟执行:一旦value 被更改,回调就会立即重新创建并执行。

所以我看到useCallback 在延迟运行的情况下不会提供显着优势。这取决于你。

[UPD] 最初是

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

  useEffect(throttled.current, [value])

但是这样throttled.current 已经绑定到初始value(of 0) 通过关闭。因此,即使在下一次渲染中,它也从未改变过。

由于闭包特性,所以在将函数推送到 useRef 时要小心。

【讨论】:

也许我错过了 useRef 的部分初始值使初始值接近 @mikes 它取决于(对于 lodash 的版本,有 leadingtrailing 选项来配置 github.com/lodash/lodash/blob/master/throttle.js) 我们可以使用useRef 创建回调并保留它,但我相信最好使用useCallback 甚至在必要时传递所需的变量,这种情况很少发生。我们可以使用setValue 来更改useCallback 中的值,而无需将value 添加到依赖数组中,甚至可以使用setValue(previous =&gt; ...) 访问之前的值。如果我们需要直接访问该值而不更改它,我们可以将其作为参数传递,就像您在示例中使用 useRef 一样,例如 useCallback(throttle((value) =&gt; ... , 1000), []) 那么这个答案的哪一部分是实际答案?有点曲折。 这个答案太混乱了,同意@coler-j【参考方案22】:

如果您在处理程序中使用它,我相当肯定这是这样做的方法。

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,
  ;

【讨论】:

【参考方案23】:

我为这个用例写了两个简单的钩子(use-throttled-effect 和use-debounced-effect),也许它对寻找简单解决方案的其他人有用。

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.log(count);     
  , 1000 ,[count]);

  return (
    count
  );

【讨论】:

完美运行,谢谢? 测试了两个钩子

以上是关于如何通过 React Hook 使用节流阀或去抖动?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Svelte 去抖动/节流?

如何创建类似于 javascript 节流/去抖动功能的 Rails/Ruby 方法

如何使用 React-Hook-Form 设置焦点

如何在 React Native 应用程序中使用 React hook useEffect 为每 5 秒渲染设置间隔?

如何使用React Hook

JS 抖动和节流