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

时间:2019-07-18 07:06:24

标签: javascript reactjs addeventlistener react-hooks

首先,使用一个类组件,它可以很好地工作并且不会引起任何问题。

但是,在带有钩子的功能组件中,每当我尝试通过滚动事件侦听器的功能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;

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

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

我为此创建了一个沙箱。

Edit React setState from event listener

5 个答案:

答案 0 :(得分:1)

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

但是,除了重新创建handleScroll之外,还将其设置为事件侦听器,只是它的第一个实例-因为您给useEffect(..., [])设置了它在挂载时仅运行一次。

一种解决方案是使用最新的scroll重新订阅handleScroll。为此,您必须在useEffect之内返回cleanup函数(现在它又返回一个订阅),并删除[]以在每个渲染器上运行它:

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

PS,最好使用passive event listener进行滚动。

答案 1 :(得分:1)

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

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

答案 2 :(得分:1)

  

简而言之,您需要添加goingUp作为useEffect的依赖项。

如果使用[],则将仅使用功能(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';
}

答案 3 :(得分:0)

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

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引起的所有变化。因此,我们不需要向pageUp依赖项中添加useEffect

Edit toggle on scroll

Hint:,您可以在throttledFunc上添加useEffect到{{1}},并且需要重新渲染组件

答案 4 :(得分:0)

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