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

zil*_*nas 20 javascript addeventlistener reactjs 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;
Run Code Online (Sandbox Code Playgroud)

试图useCallbackhandleScroll函数中使用钩子,但没有多大帮助。

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

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

从事件侦听器编辑 React setState

Yoz*_*ozi 17

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

1) []in 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;

Run Code Online (Sandbox Code Playgroud)

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

  • 并且不存在内存泄漏:每次“goingUp”更改时,它都会调用“removeEventListener” (5认同)
  • 我认为您还不应该担心 `useEffect` 性能。codesandbox 上的代码显示它足够快。如果您仍然希望进一步提高性能,您可以使用“useDispatch”来记忆侦听器。https://overreacted.io/a-complete-guide-to-useeffect/ 。但我认为在代码的简单性和性能之间这是一个糟糕的权衡 (3认同)

小智 6

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

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

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

使用自定义钩子

我建议将整个逻辑移动到自定义钩子中,这可以使您的代码更清晰,更易于重用。我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';
}
Run Code Online (Sandbox Code Playgroud)