繁体   English   中英

从滚动事件侦听器反应 setState 钩子

[英]React setState hook from scroll event listener

首先,对于类组件,这可以正常工作并且不会导致任何问题。

但是,在带有钩子的功能组件中,每当我尝试从滚动事件侦听器的函数handleScroll设置状态时,即使我使用了去抖动,我的状态也无法更新或应用程序的性能会受到严重影响。

import React, { useState, useEffect } from "react";
import debounce from "debounce";

let prevScrollY = 0;

const App = () => {
  const [goingUp, setGoingUp] = useState(false);

  const handleScroll = () => {
    const currentScrollY = window.scrollY;
    if (prevScrollY < currentScrollY && goingUp) {
      debounce(() => {
        setGoingUp(false);
      }, 1000);
    }
    if (prevScrollY > currentScrollY && !goingUp) {
      debounce(() => {
        setGoingUp(true);
      }, 1000);
    }

    prevScrollY = currentScrollY;
    console.log(goingUp, currentScrollY);
  };

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

  return (
    <div>
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
    </div>
  );
};

export default App;

尝试在handleScroll函数中使用useCallback钩子,但没有太大帮助。

我究竟做错了什么? 如何在不影响性能的情况下从handleScroll设置状态?

我已经用这个问题创建了一个沙箱。

从事件侦听器编辑 React setState

在您的代码中,我看到了几个问题:

1) useEffect 中的[]意味着它不会看到任何状态变化, goingUp变化。 它总是会看到goingUp初始值

2) debounce不起作用。 它返回一个新的去抖动函数。

3)通常全局变量是一种反模式,认为它只适用于您的情况。

4)你的滚动监听器不是被动的,正如@skyboyer 所提到的。

import React, { useState, useEffect, useRef } from "react";

const App = () => {
  const prevScrollY = useRef(0);

  const [goingUp, setGoingUp] = useState(false);

  useEffect(() => {
    const handleScroll = () => {
      const currentScrollY = window.scrollY;
      if (prevScrollY.current < currentScrollY && goingUp) {
        setGoingUp(false);
      }
      if (prevScrollY.current > currentScrollY && !goingUp) {
        setGoingUp(true);
      }

      prevScrollY.current = currentScrollY;
      console.log(goingUp, currentScrollY);
    };

    window.addEventListener("scroll", handleScroll, { passive: true });

    return () => window.removeEventListener("scroll", handleScroll);
  }, [goingUp]);

  return (
    <div>
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
    </div>
  );
};

export default App;

https://codesandbox.io/s/react-setstate-from-event-listener-q7to8

总之,需要添加goingUp作为goingUp的依赖。

如果您使用[] ,您将只使用函数( handleScroll ,在初始渲染中创建)创建/删除侦听器。 换句话说,重新渲染时,滚动事件侦听器仍在使用初始渲染时的旧handleScroll

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

使用自定义钩子

我建议将整个逻辑移动到自定义钩子中,这可以使您的代码更清晰,更易于重用。 我使用useRef来存储以前的值。

export function useScrollDirection() {
  const prevScrollY = useRef(0)
  const [goingUp, setGoingUp] = useState(false);

  const handleScroll = () => {
    const currentScrollY = window.scrollY;
    if (prevScrollY.current < currentScrollY && goingUp) {
      setGoingUp(false);
    }
    if (prevScrollY.current > currentScrollY && !goingUp) {
      setGoingUp(true);
    }
    prevScrollY.current = currentScrollY;
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [goingUp]); 
  return goingUp ? 'up' : 'down';
}

您正在每个渲染上重新创建handleScroll函数,因此它是指实际数据( goingup ),因此您在这里不需要useCallback

但是除了你重新创建handleScroll你被设置为事件监听器只有它的第一个实例 - 因为你给useEffect(..., [])它只在安装时运行一次。

一种解决方案是使用最新的handleScroll重新订阅scroll 要做到这一点,你必须在useEffect返回清理函数(现在它返回一个更多的订阅)并删除[]以在每次渲染时运行它:

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

PS,您最好使用被动事件侦听器进行滚动。

这是我对这种情况的解决方案

const [pageUp, setPageUp] = useState(false)
const lastScroll = useRef(0)

const checkScrollTop = () => {
  const currentScroll = window.pageYOffset
  setPageUp(lastScroll.current > currentScroll)
  lastScroll.current = currentScroll
}

// lodash throttle function
const throttledFunc = throttle(checkScrollTop, 400, { leading: true })

useEffect(() => {
  window.addEventListener("scroll", throttledFunc, false)
  return () => {
    window.removeEventListener("scroll", throttledFunc)
  }
}, [])

仅在componentDidMount时添加事件侦听器应该是一次。 并非在pageUp引起的所有更改中。 所以,我们不需要添加pageUpuseEffect依赖项。

滚动时编辑切换

Hint:您可以添加throttledFuncuseEffect如果它的变化,你需要重新呈现组件

const objDiv = document.getElementById('chat_body');
objDiv.scrollTop = objDiv.scrollHeight;

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM