如何通过 React Hook 使用油门或去抖动?

IT技术 reactjs lodash react-hooks throttling
2021-04-21 01:23:08

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

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

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

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

6个回答

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

下面的原始答案

您可能(并且可能需要)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>
  )
}

至于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由于闭包特性,因此在将函数推入时要小心

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

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

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

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

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

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

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

@David 结构相等只是意味着内部的值相同,具有相同的键、值等。它是值相等或其他任何你会称之为的。
2021-05-29 01:23:08
@David 函数绝对具有引用相等性,这就是您useCallback首先需要的原因你的例子是结构平等,而不是参照平等。
2021-06-01 01:23:08
如果您想知道为什么useCallback需要,我相信这就是原因:JavaScript 中的函数没有引用相等性(即() => {} === () => {} // false)。所以每次组件重新渲染时effect都和以前不一样。然而,随着useCallback的使用,你告诉 React '请只在我deps改变时才考虑我改变了!'
2021-06-02 01:23:08
@KevinBeal,我想我之前没有听说过结构平等这个词,并且在互联网上快速搜索(在 Kotlin 中)说参考是===和结构是==根据这种逻辑,我认为 JavaScript 中的函数具有结构相等性
2021-06-15 01:23:08

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洛达什

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

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


定制

1. 你可以用你自己的throttledebounce代码替换 Lodash ,比如:

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

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

好问题 - 这旨在始终包含内部的最新回调cbRef可变 ref 可以像Hooks实例变量一样使用——这里有一个setInterval来自 Overreacted 博客的例子渲染阶段也应该是纯的,没有副作用,例如兼容 React 并发模式。这就是为什么我们将作业包装在useEffect.
2021-06-11 01:23:08
使用 useThrottle (Lodash) 时,我似乎收到错误消息:“TypeError:无法读取未定义的属性‘应用’”。再加上,我有一个 ESLint 错误,说“React Hook useCallback 收到一个依赖项未知的函数。而是传递一个内联函数。”
2021-06-13 01:23:08
为什么使用 useEffect(() => { cbRef.current = cb; }); 没有任何依赖?这意味着我们在每次重新渲染时都运行 effect,那么为什么不简单地分配而不使用 useEffect 呢?
2021-06-19 01:23:08

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

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

如果你的意思是useDebounce与 with 一起使用useDidMountEffect,你只需要在上面的例子中替换useEffectwithuseDidMountEffect就可以了。
2021-05-29 01:23:08
@andreapier 我已经添加了另一个自定义钩子的链接,以防止在初始渲染时渲染,如果你没有看到它,这里是链接:stackoverflow.com/a/57941438/3367974
2021-06-07 01:23:08
我不明白如何避免第二次(或第一次)渲染,即使使用链接的钩子。你能提供一个例子吗?谢谢
2021-06-09 01:23:08
是的,我看见它了。我的问题是如何让两者一起工作。但是,我转向了另一种解决方案,因为这个(在我看来)存在太多问题。
2021-06-12 01:23:08

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