[英]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
设置状态?
我已经用这个问题创建了一个沙箱。
在您的代码中,我看到了几个问题:
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
引起的所有更改中。 所以,我们不需要添加pageUp
到useEffect
依赖项。
Hint:
您可以添加throttledFunc
到useEffect
如果它的变化,你需要重新呈现组件
const objDiv = document.getElementById('chat_body');
objDiv.scrollTop = objDiv.scrollHeight;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.