【问题标题】:How to use throttle or debounce with React Hook?如何通过 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 函数中调用它?
  • 是的,我试过了,它可以工作,但就我而言,它不是很优雅,因为我在节流方法中使用了组件变量。

标签: reactjs lodash react-hooks throttling


【解决方案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 我已经添加了指向另一个自定义钩子的链接,以防止在初始渲染时进行渲染,因为您没有看到它,这里是链接:stackoverflow.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-effectuse-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}
                                    );
                                  }
                                  

                                  【讨论】:

                                  • 完美运行,谢谢? 测试了两个钩子
                                  猜你喜欢
                                  • 2018-02-16
                                  • 2021-08-19
                                  • 2021-07-09
                                  • 2021-08-04
                                  • 2015-07-25
                                  • 2019-11-11
                                  • 2012-07-13
                                  • 2016-05-21
                                  • 2017-05-03
                                  相关资源
                                  最近更新 更多