【问题标题】:useEffect with debounceuseEffect with debounce
【发布时间】:2019-12-24 03:11:16
【问题描述】:

我正在尝试创建一个其值去抖动的输入字段(以避免不必要的服务器访问)。 第一次渲染我的组件时,我从服务器获取它的值(有一个加载状态和所有)。

这是我所拥有的(为了示例的目的,我省略了不相关的代码)。

这是我的去抖钩子:

export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

(我从:https://usehooks.com/useDebounce/ 得到这个)

对,这是我的组件以及我如何使用 useDebounce 钩子:

function ExampleTitleInput(props) {
  const [title, setTitle] = useState(props.title || "");
  const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
  const [commitsCount, setCommitsCount] = useState(0);

  const debouncedTitle = useDebounce(title, 1000);

  useEffect(() => {
    setTitle(props.title || "");
  }, [props.title]);

  useEffect(() => {
    if (debouncedTitle !== lastCommittedTitle) {
      setLastCommittedTitle(debouncedTitle);
      setCommitsCount(commitsCount + 1);
    }
  }, [debouncedTitle, lastCommittedTitle, commitsCount]);

  return (
    <div className="example-input-container">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <div>Last Committed Value: {lastCommittedTitle}</div>
      <div>Commits: {commitsCount}</div>
    </div>
  );
}

这里是父组件:

function App() {
  const [title, setTitle] = useState("");

  useEffect(() => {
    setTimeout(() => setTitle("This came async from the server"), 2000);
  }, []);

  return (
    <div className="App">
      <h1>Example</h1>
      <ExampleTitleInput title={title} />
    </div>
  );
}

当我运行这段代码时,我希望它在第一次(仅)时忽略去抖动值的变化,所以它应该显示提交次数为 0,因为该值是从 props 传递的。应跟踪任何其他更改。抱歉,我度过了漫长的一天,此时我有点困惑(我想我一直盯着这个“问题”太久了)。

我已经创建了一个示例:

https://codesandbox.io/s/zen-dust-mih5d

它应该显示提交的数量为 0,并且值设置正确,没有去抖改变。

我希望我说得通,如果我能提供更多信息,请告诉我。

编辑

这完全符合我的预期,但是它给了我“警告”(注意 deps 数组中缺少依赖项):

function ExampleTitleInput(props) {
  const [title, setTitle] = useState(props.title || "");
  const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
  const [commitsCount, setCommitsCount] = useState(0);

  const debouncedTitle = useDebounce(title, 1000);

  useEffect(() => {
    setTitle(props.title || "");
    // I added this line here
    setLastCommittedTitle(props.title || "");
  }, [props]);

  useEffect(() => {
    if (debouncedTitle !== lastCommittedTitle) {
      setLastCommittedTitle(debouncedTitle);
      setCommitsCount(commitsCount + 1);
    }
  }, [debouncedTitle]); // removed the rest of the dependencies here, but now eslint is complaining and giving me a warning that I use dependencies that are not listed in the deps array

  return (
    <div className="example-input-container">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <div>Last Committed Value: {lastCommittedTitle}</div>
      <div>Commits: {commitsCount}</div>
    </div>
  );
}

这里是:https://codesandbox.io/s/optimistic-perlman-w8uug

这行得通,很好,但我担心警告,感觉好像我做错了什么。

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    检查我们是否在第一次渲染中的一种简单方法是设置一个在循环结束时更改的变量。您可以在组件中使用 ref 来实现这一点:

    const myComponent = () => {
        const is_first_render = useRef(true);
    
        useEffect(() => {
            is_first_render.current = false;
        }, []);
    
        // ...
    

    您可以将其提取到一个钩子中,然后简单地将其导入您的组件中:

    const useIsFirstRender = () => {
        const is_first_render = useRef(true);
    
        useEffect(() => {
            is_first_render.current = false;
        }, []);
    
        return is_first_render.current;
    };
    

    然后在你的组件中:

    function ExampleTitleInput(props) {
        const [title, setTitle] = useState(props.title || "");
        const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
        const [updatesCount, setUpdatesCount] = useState(0);
        const is_first_render = useIsFirstRender(); // Here
    
        const debouncedTitle = useDebounce(title, 1000);
    
        useEffect(() => {
            setTitle(props.title || "");
        }, [props.title]);
    
        useEffect(() => {
            // I don't want this to trigger when the value is passed by the props (i.e. - when initialized)
            if (is_first_render) { // Here
                return;
            }
    
            if (debouncedTitle !== lastCommittedTitle) {
                setLastCommittedTitle(debouncedTitle);
                setUpdatesCount(updatesCount + 1);
            }
        }, [debouncedTitle, lastCommittedTitle, updatesCount]);
    
        // ...
    

    【讨论】:

    • 我认为这是一个很好的例子,说明了 useEffect 的实际工作原理以及如何将 debounce 与 useEffect 函数一起使用。 cmets 的放置方式足以弄清楚发生了什么。添加必要的导入语句以完成整个代码可能会很有用。
    【解决方案2】:

    您可以更改useDebounce 挂钩以了解应立即设置第一个设置的去抖动值这一事实。 useRef 非常适合:

    export function useDebounce(value, delay) {
      const [debouncedValue, setDebouncedValue] = useState(value);
      const firstDebounce = useRef(true);
    
      useEffect(() => {
        if (value && firstDebounce.current) {
          setDebouncedValue(value);
          firstDebounce.current = false;
          return;
        }
    
        const handler = setTimeout(() => {
          setDebouncedValue(value);
        }, delay);
    
        return () => clearTimeout(handler);
      }, [value, delay]);
    
      return debouncedValue;
    }
    

    【讨论】:

    • 感谢您的帮助,非常感谢。我没有想到在去抖钩子中使用useRef。它绝对“感觉”是正确的解决方案(特别是因为我在其他组件中使用它并且我有完全相同的问题),但是我无法真正让它工作,它是(也许我是缺少一些东西):codesandbox.io/s/musing-taussig-piwii
    • 另外,如果要问的不是太多 - 你有什么办法可以看看我的编辑并告诉我这种方法是否错误?再次感谢,真的。
    • 好吧,“提交”计数应该是 0。
    【解决方案3】:

    我认为您可以通过某些方式改进您的代码:

    首先,不要使用 useEffect 将 props.title 复制到 ExampleTitleInput 中的本地状态,因为它可能会导致过度重新渲染(第一次用于更改道具,而不是更改状态作为副作用)。直接使用props.title,将去抖动/状态管理部分移至父组件。您只需将onChange 回调作为道具传递(考虑使用useCallback)。

    要跟踪旧状态,正确的钩子是useRef (API reference)

    如果你不希望它在第一次渲染中触发,你可以使用自定义钩子,例如useUpdateEffect,来自react-use:https://github.com/streamich/react-use/blob/master/src/useUpdateEffect.ts,它已经实现了useRef相关逻辑。

    【讨论】:

    • 您好,谢谢您的帮助。您能否澄清一下,“直接”使用props.title 是什么意思?您的意思是从组件中完全删除状态并将其拉到父级吗?
    • 是的,完全正确。将其转换为受控输入 (reactjs.org/docs/forms.html#controlled-components)
    猜你喜欢
    • 2020-08-30
    • 2021-11-14
    • 2018-10-11
    • 2017-08-06
    • 2021-03-23
    • 2019-12-25
    • 2021-09-08
    • 1970-01-01
    • 2023-02-19
    相关资源
    最近更新 更多