如何在 React Hook 中使用 throttle 或 debounce?

How to use throttle or debounce with React Hook?

我正在尝试在功能组件中使用 lodash 中的 throttle 方法,例如:

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

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

有人有简单的解决办法吗?

经过一段时间后,我确信使用 setTimeout/clearTimeout(并将其移至单独的自定义挂钩)自己处理事情比使用功能性助手更容易。稍后处理会在我们将其应用于 useCallback 之后立即产生额外的挑战,由于依赖项更改可以重新创建,但我们不想重置延迟 运行ning.

原回答如下

你可能(并且可能需要)useRef to store value between renders. Just like it's 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(0)。因此,即使在下一次渲染中也从未更改过。

由于闭包特性,因此在将函数推入 useRef 时要小心。

我为此用例编写了两个简单的钩子 (use-throttled-effect and 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}
  );
}

如果您在处理程序中使用它,我相当确定这是实现它的方法。

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

它可以是一个很小的自定义挂钩,如下所示:

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

就我而言,我还需要通过该事件。这样做:

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

我创建了自己的自定义挂钩 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]);
}

除非您想看到警告,否则需要禁用 exhaustive-deps 的注释,因为 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

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。您可以用自己的 throttledebounce 代码替换 Lodash,例如:

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>

这里使用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} />

我来晚了,但这里有一种去抖动的方法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.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} />

我想使用 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.current)
        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>

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

react-tablehttps://react-table.tanstack.com/docs/faq#how-can-i-debounce-rapid-table-state-changes

中有一个不错的 useAsyncDebounce 函数

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

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

我的解决方案类似于此 (功能 useMemo),但是我将参数直接传递给 useEffect 中的去抖动函数,而不是将其视为依赖项。它通过分离参数(应该是 re-created)和去抖函数(不应该是 re-created)解决了 re-creating 钩子的问题。

const MyComponent: FC<Props> = ({ handler, title }) => {
  const payload = useMemo<Payload>(() => ({ title }), [title])
  const debouncedHandler = useMemo(() => debounce(handler, 1000), [handler])

  useEffect(() => debouncedHandler(payload), [payload, debouncedHandler])
}
function myThrottle(callback, delay) {
  var previousTime = 0;
  return function (...args) {
    let currentTime = Date.now();
    let gap = currentTime - previousTime;
    if (gap > 0) {
      previousTime = currentTime + delay;
      callback.call(this, ...args);
    }
    return;
  };
}

在您的功能组件中使用以下代码。

const memoizedCallback = useMemo(() => myThrottle(callback, 3000), []);

使用memoizedCallback作为回调