如何在React Hook中使用油门或反跳?

Ale*_*nic 24 throttling lodash reactjs react-hooks

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

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

由于内部方法useEffect在每次渲染时都会重新声明,因此限制效果不起作用。

有没有人有一个简单的解决方案?

Tod*_*ton 47

我创建了自己的自定义钩子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;
Run Code Online (Sandbox Code Playgroud)

useDebouncedEffect.js

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;
Run Code Online (Sandbox Code Playgroud)

除非您想看到警告,否则禁用详尽的 deps 的注释是必需的,因为 lint 总是会抱怨作为依赖项没有效果。添加 effect 作为依赖项将在每次渲染时触发 useEffect。相反,您可以添加检查以useDebouncedEffect确保它已通过所有依赖项。(见下文)

将详尽的依赖项检查添加到 useDebouncedEffect

如果你想让 eslint 检查useDebouncedEffect详尽的依赖项,你可以将它添加到 eslint 配置中package.json

  "eslintConfig": {
    "extends": [
      "react-app"
    ],
    "rules": {
      "react-hooks/exhaustive-deps": ["warn", {
        "additionalHooks": "useDebouncedEffect"
      }]
    }
  },
Run Code Online (Sandbox Code Playgroud)

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

  • 如果你想知道为什么需要 `useCallback`,我相信这就是原因:JavaScript 中的函数没有引用相等性(即 `() =&gt; {} === () =&gt; {} // false`)。所以组件每次重新渲染的效果都和之前不一样。然而,通过使用“useCallback”,你告诉 React“请只在我的“deps”也发生变化时才考虑我的变化!” (5认同)
  • @David 函数绝对具有引用相等性,这就是为什么您首先需要 `useCallback` 的原因。你的例子是结构平等,而不是参照平等。 (2认同)

sky*_*yer 34

您可能(可能需要)useRef在渲染之间存储值。就像对计时器建议一样

像这样

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>
  )
}
Run Code Online (Sandbox Code Playgroud)

至于useCallback

它可能也可以工作

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []);
Run Code Online (Sandbox Code Playgroud)

但是,如果value更改后尝试重新创建回调,则:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]);
Run Code Online (Sandbox Code Playgroud)

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

因此,我发现useCallback在延迟运行的情况下不会提供明显的优势。由你决定。

[UPD]最初是

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

  useEffect(throttled.current, [value])
Run Code Online (Sandbox Code Playgroud)

但是这种方式throttled.currentvalue通过闭包绑定到初始(0)。因此,即使在下一个渲染中也从未改变过。

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

  • 那么这个答案的哪一部分才是真正的答案呢?有点曲折。 (3认同)
  • 这个答案太混乱了,同意@coler-j (3认同)
  • 我们可以使用`useRef`来创建回调并保留它,但我相信最好使用`useCallback`甚至在必要时传递所需的变量,但这种情况很少发生。我们可以使用 setValue 来更改 useCallback 中的值,而无需将 value 添加到依赖项数组中,甚至可以使用 setValue(previous =&gt; ...) 来访问之前的值。如果我们需要直接访问该值而不更改它,我们可以将其作为参数传递,就像您在示例中使用“useRef”一样,例如“useCallback(throttle((value) =&gt; { ... }, 1000”) ),[])`。 (2认同)

for*_*d04 20

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
};
Run Code Online (Sandbox Code Playgroud)

useThrottle洛达什

import _ from "lodash"

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

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
};
Run Code Online (Sandbox Code Playgroud)
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]
  );
}
Run Code Online (Sandbox Code Playgroud)

useDebounce洛达什

import _ from "lodash"

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

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"));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


定制

1. 你可以用你自己的throttledebounce代码替换 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]
  );
}
Run Code Online (Sandbox Code Playgroud)
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"));
Run Code Online (Sandbox Code Playgroud)

2.useThrottle可以缩短,如果总是与useEffect(相同的useDebounce)一起使用:

const App = () => {
  // useEffect now is contained inside useThrottle
  useThrottle(() => console.log(value), 1000, [value]);
  // ...
};
Run Code Online (Sandbox Code Playgroud)

<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>
Run Code Online (Sandbox Code Playgroud)
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"));
Run Code Online (Sandbox Code Playgroud)

  • 好问题 - 这旨在始终包含“cbRef”内的最新回调。可变引用可以像 Hooks 的[实例变量](https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables)一样使用 - [此处](https: //overreacted.io/making-setinterval-declarative-with-react-hooks/#just-show-me-the-code) 是 Overreacted 博客中的 `setInterval` 示例。渲染阶段也应该是纯粹的,没有副作用,例如与React并发模式兼容。这就是为什么我们将赋值包装在 `useEffect` 中。 (3认同)

Meh*_*ani 16

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

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;
}
Run Code Online (Sandbox Code Playgroud)

用法示例:

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 (
        // ...
    );
}
// ...
Run Code Online (Sandbox Code Playgroud)

注意:您可能知道,useEffect始终在初始渲染上运行,因此如果您使用我的答案,您可能会看到组件的渲染运行两次,别担心,您只需要编写另一个自定义钩子。查看我的其他答案以获取更多信息。


gaz*_*rgo 7

我想使用以下方法加入我的节流和去抖输入的聚会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'))
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


Sam*_*adi 6

我为此用例编写了两个简单的钩子(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}
  );
}
Run Code Online (Sandbox Code Playgroud)


Ali*_*sei 6

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

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;
}
Run Code Online (Sandbox Code Playgroud)

和用法:

const throttledFunc = useThrottle(someFunc, 200);
Run Code Online (Sandbox Code Playgroud)

希望这会对某人有所帮助。


Jas*_*ngh 5

在 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>;
}
Run Code Online (Sandbox Code Playgroud)

  • 正确答案。这是最直接的解决方案。 (4认同)