【问题标题】:How to use useState in useEffect?如何在useEffect中使用useState?
【发布时间】:2021-07-18 16:54:09
【问题描述】:

我尝试将我的组件从 react.component 重构为钩子,但我遇到了一些麻烦。我真的不明白如何使用我的状态offsetTop。该值已设置,或在我需要时设置。

第一次尝试:

const [offsetTop, setOffsetTop] = useState();

useEffect(() => {
    setOffsetTop(window.pageYOffset + document.querySelector(".projectsContainer").getBoundingClientRect().top)
    console.log(offsetTop);
    window.addEventListener('scroll', handleScroll, true)
    return () => window.removeEventListener("scroll", handleScroll);
}, []);

我还尝试将它与useEffect[offsetTop] 作为第二个参数的第二种方法一起使用,但我的日志显示了两次 undef 和值。我明白为什么它显示两次,但仍然没有解决我的问题,我的情况不起作用。

第二次尝试:

const [offsetTop, setOffsetTop] = useState();

useEffect(() => {
    setOffsetTop(window.pageYOffset + document.querySelector(".projectsContainer").getBoundingClientRect().top)
}, []);

useEffect(() => {
    console.log(offsetTop);
    window.addEventListener('scroll', handleScroll, true)
    return () => window.removeEventListener("scroll", handleScroll);
}, [offsetTop]);

handleScroll 功能:

function handleScroll() {
    console.log(offsetTop);
    const scrolledY = window.scrollY
    if (scrolledY > offsetTop) console.log("works")
}

与放在componentDidMount() {} 中的First try: 相同的代码并使用this 可以正常工作。所以不好的不是条件。这是关于何时设置值以及useStateuseEffect 的工作原理。

【问题讨论】:

  • 你确定.projectsContainer在useEffect运行时可以被查询吗?为了确保它是你可以尝试 useLayoutEffect 代替,它在所有 DOM 突变后同步运行

标签: javascript reactjs react-hooks next.js


【解决方案1】:

您的条件不适用于第一个变体吗?那个看起来是正确的,在这种情况下,console.log(offsetTop) 不应该打印新设置的值,而是之前设置的值。要按照您的需要记录它:

const [offsetTop, setOffsetTop] = useState();

useEffect(() => {
    setOffsetTop(window.pageYOffset + document.querySelector(".projectsContainer").getBoundingClientRect().top)
    window.addEventListener('scroll', handleScroll, true)
    return () => window.removeEventListener("scroll", handleScroll);
}, []);

useEffect(() => {
    console.log(offsetTop);
}, [offsetTop]);

您第二次尝试的问题在于它执行以下操作:

  • 第一次渲染:
    • 第一次使用效果设置新offsetTop
    • 第二次使用旧的offsetTop触发
  • offsetTop 更新导致重新渲染
    • 第二次 useEffect 进行清理,移除事件监听器
    • 我猜,浏览器在此处传播事件,而您的监听器不见了
    • 第二次 useEffect 第二次触发,使用新的offsetTop,并再次设置您的侦听器(但它已经错过了事件)

啊……这只是你问题的一部分。

您的问题的第二部分是 handleScroll() 从创建并设置为侦听器的渲染周期中有效地捕获 offsetTop 值。您应该使用useRef() 使handleScroll() 与当前值保持同步(如果您因为其他原因不需要offsetTop,则根本不需要使用该状态。您可以:

const { current: heap } = useRef({
  offsetTop: 0,
});

useEffect(() => {
  function handleScroll() {
    console.log(heap.offsetTop);
    const scrolledY = window.scrollY
    if(scrolledY > heap.offsetTop) console.log("works")
  }
  heap.offsetTop = window.pageYOffset + document.querySelector(".projectsContainer").getBoundingClientRect().top;
  window.addEventListener('scroll', handleScroll, true)
  return () => window.removeEventListener("scroll", handleScroll);
  // The heap is guaranteed to be the same object across re-renders,
  // thus including it into dependencies below does not cause useEffect
  // to recompute. However, if you don't include it, and rely on ESLint
  // to check code errors, it will complain about heap not being there
  // as missing dependency.
}, [heap]);

这样,offsetTop 在重新渲染中保持不变,handleScroll() 始终使用当前值。如果您还需要在 offsetTop 更新时重新渲染,那么您还可以使用 useState() 来触发重新渲染(比如说还要将 offsetTop 值注入到渲染的组件中。

【讨论】:

    【解决方案2】:

    这让我觉得你可能根本不需要状态。为什么不根据需要导出offsetTop 的值呢?

    useEffect(() => {
      function handleScroll() {
        const scrolledY = window.scrollY;
        const offsetTop = window.pageYOffset + document.querySelector(".projectsContainer").getBoundingClientRect().top;
        if(scrolledY > offsetTop) console.log("works")
      }
    
      window.addEventListener('scroll', handleScroll, true)
      return () => window.removeEventListener("scroll", handleScroll, true);
    }, []);
    

    但是,也许这个想法是您想要捕获offsetTop 的初始值,然后在事情发生变化时使用它。在这种情况下,我认为您的主要问题是由于removeEventListener 没有被正确清理,因为您没有传递与addEventListener 相同的参数(缺少true)。因此,仍然有一个 handleScroll 的实例在第一次渲染时持有 offsetTop 的初始值,我认为这就是您看到 undefined 被记录的原因。

    错了❌

    useEffect(() => {
      setOffsetTop(window.pageYOffset +
          document.querySelector(".projectsContainer").getBoundingClientRect().top;);
    }, []);
    
    useEffect(() => {
      // The first time this useEffect runs, the value of `offsetTop`
      // is still the initial value of undefined.
      function handleScroll() {
        console.log(offsetTop)
        const scrolledY = window.scrollY;
        if (scrolledY > offsetTop) console.log("works");
      }
    
      window.addEventListener("scroll", handleScroll, true);
      // This doesn't get removed correctly
      // because you don't pass the same parameters (missing `true`)
      return () => window.removeEventListener("scroll", handleScroll); 
    }, [offsetTop]);
    

    对 ✅

    useEffect(() => {
      setOffsetTop(window.pageYOffset +
          document.querySelector(".projectsContainer").getBoundingClientRect().top;);
    }, []);
    
    useEffect(() => {
      function handleScroll() {
        const scrolledY = window.scrollY;
        if (scrolledY > offsetTop) console.log("works");
      }
    
      window.addEventListener("scroll", handleScroll, true);
      return () => window.removeEventListener("scroll", handleScroll, true);
    }, [offsetTop]);
    

    复制: https://codesandbox.io/s/so-67247616-pl455

    【讨论】:

    • 你的正确变体有这个问题:它在每次更新 offsetTop 时删除并重新添加事件侦听器,这在性能方面很糟糕,也会错过一些中间的更新(当上一个侦听器已删除,但尚未添加新的侦听器)。 removeEventListener(..) 的参数必须与 addEventListener(..) 的参数匹配 - 我错过了:)
    猜你喜欢
    • 2020-08-14
    • 2021-04-10
    • 2020-10-20
    • 1970-01-01
    • 2021-03-07
    • 2021-09-06
    • 2020-02-14
    • 2021-12-04
    • 1970-01-01
    相关资源
    最近更新 更多