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

React setState hook from scroll event listener

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

但是,在带有钩子的功能组件中,每当我尝试从我的滚动事件侦听器的函数 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 设置状态?

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

你在每个渲染器上都是 re-creating handleScroll 函数,所以它指的是实际数据 (goingup) 所以你在这里不需要 useCallback

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

一种解决方案是 re-subscribe 到 scroll 和 up-to-date handleScroll。为此,您必须在 useEffect 中 return 清理函数(现在它 return 又订阅了一个),并在每次渲染时将 [] 删除到 运行 :

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

PS 最好使用 passive event listener 滚动。

在你的代码中我发现了几个问题:

1) useEffect 中的 [] 意味着它不会看到任何状态变化,例如 goingUp 的变化。它将始终看到 goingUp

的初始值

2) debounce 这样不行。它 returns 一个新的去抖功能。

3) 通常全局变量是一个 anti-pattern,认为它只适用于您的情况。

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

In short, you need to add goingUp as the dependency of useEffect.

如果你使用 [],你只会 create/remove 一个带有函数的监听器(handleScroll,它是在初始渲染中创建的)。换句话说,当 re-render 时,滚动事件侦听器仍在使用初始渲染中的旧 handleScroll

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

Using custom hooks

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

这是我针对这个案例的解决方案

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: 您可以将 throttledFunc 添加到 useEffect 如果它发生变化并且您需要 re-render 组件

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