【问题标题】:React hook not updated in function listened on scroll event在滚动事件上侦听的函数中未更新 React 钩子
【发布时间】:2019-11-02 12:06:16
【问题描述】:

我有一个函数handleScroll 监听滚动事件。该函数必须更新 isFetching(以 false 开头并且必须更改布尔值)。

handleScroll 函数被正确监听,如console.log 所示。但是,isFetching 始终为假。 似乎从未阅读过setIsFetching。我认为另一种选择就像 eventListener 冻结了 handleScroll 函数的第一个版本。

如何才能更新该函数中的挂钩? 这是代码的简化版本和codesandbox

/* <div id='root'></div> */
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const debounce = (func, wait, immediate) => {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      timeout = null;
      if (!immediate) func.apply(context, args);
    }, wait);
    if (immediate && !timeout) func.apply(context, args);
  };
};
const App = () => {
  const [isFetching, setIsFetching] = useState(false);
  const handleScroll = debounce(() => {
    setIsFetching(!isFetching);
    console.log({ isFetching });
  }, 300);
  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);
  return <div style={{ height: "1280px" }}>Hello world</div>;
};
const root = document.getElementById("root");
if (root) ReactDOM.render(<App />, root);

更新

我将一个空数组作为第二个参数放在 useEffect 中,因为我希望第一个参数函数只在 componentDidMount() 上触发一次

【问题讨论】:

    标签: javascript reactjs events react-hooks


    【解决方案1】:

    isFetching 添加为useEffect 的依赖项

    虽然我无法提供深入的解释,但我可以说你在 useEffect 中基本上对 React 撒了谎,当你说效果不依赖于任何东西时,通过提供一个空的依赖数组总是很好的传递所有包含在您的效果中的变量。

    此外,您每次组件re-render 时都会创建一个新函数,以避免这种情况将函数移到useEffect 内或将其包装在useCallback 内,除非数组中的某些内容否则不会创建重新创建函数依赖变化

    useEffect(
      () => {
        const handleScroll = debounce(() => {
          setIsFetching(prevState => !prevState);
          console.log({ isFetching });
        }, 300);
        window.addEventListener("scroll", handleScroll);
        return () => window.removeEventListener("scroll", handleScroll);
      },
      [isFetching]
    );
    

    useCallback

    const handleScroll = useCallback(
      debounce(() => {
        setIsFetching(prevState => !prevState);
        console.log({ isFetching });
      }, 300),
      [isFetching]
    );
    
    useEffect(
      () => {
        window.addEventListener("scroll", handleScroll);
        return () => window.removeEventListener("scroll", handleScroll);
      },
      [isFetching]
    );
    

    complete guide to useEffect

    【讨论】:

    • 感谢@Asaf!您的代码也有效!我给你投了赞成票。顺便说一句,我将使用另一个选项,因为我希望 useEffect 只在 componentDidMount() 处触发一次。我忘了提。我用这个信息编辑了这个问题。使用此解决方案,该函数在每次 isFetching 更新时都会覆盖。在某些情况下,您不想要它。例如,假设您正在咨询 API。
    • @NicoDiz 这真的取决于你想做什么,但是删除监听器并添加一个新的监听器是完全可以的,而接受的答案确实解决了问题,我不认为它是处理这个问题的“反应方式”。
    【解决方案2】:

    为了从 useEffect 回调中监听你的状态变化(当你没有关注任何道具更新时),你可以将你的状态保存在组件范围之外的变量中,并使用它而不是直接的状态。

    这里有代码:

    import React, { useState, useEffect } from "react";
    import ReactDOM from "react-dom";
    
    const debounce = (func, wait, immediate) => {
      let timeout;
      return function() {
        const context = this;
        const args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          timeout = null;
          if (!immediate) func.apply(context, args);
        }, wait);
        if (immediate && !timeout) func.apply(context, args);
      };
    };
    
    let isFetchingState;
    
    const App = () => {
      const [isFetching, setIsFetching] = useState(false);
    
      isFetchingState = isFetching;
    
      const handleScroll = debounce(() => {
        setIsFetching(!isFetchingState);
        console.log({ isFetchingState });
      }, 300);
    
      useEffect(() => {
        window.addEventListener("scroll", handleScroll);
        return () => window.removeEventListener("scroll", handleScroll);
      }, []);
    
      return <div style={{ height: "1280px" }}>Hello world</div>;
    };
    
    const root = document.getElementById("root");
    
    if (root) ReactDOM.render(<App />, root);
    

    【讨论】:

      【解决方案3】:

      您已将一个空数组作为第二个参数传递给 useEffect。这就是你在运行后不再被称为 useEffect 的关键问题。

      要解决问题,请不要传递第二个参数。

       useEffect(() => {
          window.addEventListener("scroll", handleScroll);
          return () => window.removeEventListener("scroll", handleScroll);
        });
      

      或者,每当属性被更改时调用 useEffect:

      useEffect(() => {
          window.addEventListener("scroll", handleScroll);
          return () => window.removeEventListener("scroll", handleScroll);
        }, [isFetching]); // re-run useEffect on isFetching changed
      

      这类似于我们在 componentDidUpdate 中所做的:

      if (prevState.count !== this.state.count) {
        // do the stuff
      

      有关更多详细信息,请参阅documentation 本身。

      来自文档的注释:

      如果你想运行一个效果并且只清理一次(在挂载和卸载时),你可以传递一个空数组([])作为第二个参数。这告诉 React 你的效果不依赖于任何来自 props 或 state 的值,所以它永远不需要重新运行。这不是作为特殊情况处理的——它直接遵循依赖项数组的工作方式。

      如果传递一个空数组([]),则效果内的道具和状态将始终具有其初始值。虽然将 [] 作为第二个参数传递更接近于熟悉的 componentDidMount 和 componentWillUnmount 心理模型,但通常有更好的解决方案来避免过于频繁地重新运行效果。另外,不要忘记 React 会延迟运行 useEffect 直到浏览器绘制完成,所以做额外的工作不是问题。

      【讨论】:

      • 感谢@Bhojendra!您的代码也有效!我给你投了赞成票。顺便说一句,我将使用另一个选项,因为我希望 useEffect 只在 componentDidMount() 处触发一次。我忘了提。我用这个信息编辑了这个问题。对@Asaf 的评论相同。也感谢您的文档解释!
      猜你喜欢
      • 2019-11-27
      • 2023-04-02
      • 1970-01-01
      • 2020-02-12
      • 1970-01-01
      • 2015-05-23
      • 2022-01-28
      • 2023-02-09
      • 1970-01-01
      相关资源
      最近更新 更多