英文:
How to remove event listener inside useCallback hook
问题
我想创建一个通用的React钩子,它将向元素添加滚动事件并返回一个布尔值,指示用户是否已经滚动到元素的顶部。现在,问题是这个元素可能不会立即可见。因此,我不能使用useEffect
。据我了解,在这种情况下,建议使用useCallback
。
所以我这样做,它可以工作:
function useHasScrolled() {
const [hasScrolled, setHasScrolled] = useState(false);
const ref = useRef(null);
const setRef = useCallback((element) => {
const handleScroll = (e) => {
setHasScrolled(e.target.scrollTop !== 0);
};
if (element) {
element.addEventListener("scroll", handleScroll);
}
ref.current = element;
}, []);
return {
hasScrolled,
scrollingElementRef: setRef
};
}
我可以像这样使用我的钩子:
const { hasScrolled, scrollingElementRef } = useHasScrolled();
....
return <div ref={scrollingElementRef}>....
然而,问题是,我不知道如何移除事件侦听器。使用useEffect
钩子很简单 - 只需返回清除函数。
如果你想查看实现,这是Codesandbox链接:https://codesandbox.io/s/pedantic-dhawan-83fdw3
英文:
I want to create a generic react hook that will add a scroll event to the element and return a boolean indicating that the user has scrolled to the top of the element.
Now, the problem is this element might not be visible right away. Hence I'm not able to use useEffect. As I understand in that situation it is advised to use useCallback
So I did, and it works:
function useHasScrolled() {
const [hasScrolled, setHasScrolled] = useState(false);
const ref = useRef(null);
const setRef = useCallback((element) => {
const handleScroll = (e) => {
setHasScrolled(e.target.scrollTop !== 0);
};
if (element) {
element.addEventListener("scroll", handleScroll);
}
ref.current = element;
}, []);
return {
hasScrolled,
scrollingElementRef: setRef
};
}
I can use my hook like this:
const { hasScrolled, scrollingElementRef } = useHasScrolled();
....
return <div ref={scrollingElementRef}>....
However, the problem is, I don't know how to remove the event listener. With the useEffect hook, it's pretty straightforward - you just return the cleanup function.
Here's the codesandbox, if you want to check the implementation: https://codesandbox.io/s/pedantic-dhawan-83fdw3
答案1
得分: 1
预期行为 - 当从DOM中移除节点时,事件监听器也将被移除并由GC收集。
但是,Codesandbox示例有点棘手,React将
<div>加载中...</div>
和
<div className="scrollingDiv" ref={scrollingElementRef}>
<h1>你好,我终于加载成功了!</h1>
<Lorem />
</div>
视为同一个div,同一个对象,只是具有不同的props(className和children),因此当div.scrollingDiv被条件渲染为div(loading)时,事件监听器仍然存在并积累。
这个行为可以通过使用key
来修复。
{loading ? (
<div key="div1">加载中...</div>
) : (
<div key="div2" className="scrollingDiv" ref={scrollingElementRef}>
<h1>你好,我终于加载成功了!</h1>
<Lorem />
</div>
)}
这样事件监听器将如预期般被移除。
另一种解决方法是在自定义钩子中添加1个额外的useRef和useEffect以存储和执行实际的取消订阅函数:
function useHasScrolled() {
const [hasScrolled, setHasScrolled] = useState(false);
const ref = useRef(null);
const unsubscribeRef = useRef(null);
const setRef = useCallback((element) => {
const eventName = "scroll";
const handleScroll = (e) => {
setHasScrolled(e.target.scrollTop !== 0);
};
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
if (element) {
element.addEventListener(eventName, handleScroll);
unsubscribeRef.current = () => {
console.log("removeEventListener called on: ", element);
element.removeEventListener(eventName, handleScroll);
};
ref.current = element;
} else {
unsubscribeRef.current = null;
ref.current = null;
}
}, []);
useEffect(() => {
return () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
};
}, []);
return {
hasScrolled,
scrollingElementRef: setRef
};
}
这段代码可以在不添加key
的情况下工作。
用于Chrome开发工具控制台的实用代码,用于计算滚动监听器的数量:
Array.from(document.querySelectorAll('*'))
.reduce(function(pre, dom){
var clks = getEventListeners(dom).scroll;
pre += clks ? clks.length || 0 : 0;
return pre
}, 0)
更新的Codesandbox链接:https://codesandbox.io/s/angry-einstein-6fb1u4?file=/src/App.js
英文:
Expected behavior - when node is removed from DOM - event listeners will be also removed and collected by GC.
But
Codesandbox example is a bit tricky, React treats
<div>Loading...</div>
and
<div className="scrollingDiv" ref={scrollingElementRef}>
<h1>Hello, I've finally loaded!</h1>
<Lorem />
</div>
as a same div, same object, just with different props (className and children), so when div.scrollingDiv is replaced by conditional rendering to div(loading) - event listeners are still there and accumulating.
This behavior can be fixed as is by using key
s.
{loading ? (
<div key="div1">Loading...</div>
) : (
<div key="div2" className="scrollingDiv" ref={scrollingElementRef}>
<h1>Hello, I've finally loaded!</h1>
<Lorem />
</div>
)}
In that way event listeners will be removed as expected.
Another solution is to add 1 more useRef and useEffect to the custom hook to store and execute actual unsubscribe function:
function useHasScrolled() {
const [hasScrolled, setHasScrolled] = useState(false);
const ref = useRef(null);
const unsubscribeRef = useRef(null);
const setRef = useCallback((element) => {
const eventName = "scroll";
const handleScroll = (e) => {
setHasScrolled(e.target.scrollTop !== 0);
};
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
if (element) {
element.addEventListener(eventName, handleScroll);
unsubscribeRef.current = () => {
console.log("removeEventListener called on: ", element);
element.removeEventListener(eventName, handleScroll);
};
ref.current = element;
} else {
unsubscribeRef.current = null;
ref.current = null;
}
}, []);
useEffect(() => {
return () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
};
}, []);
return {
hasScrolled,
scrollingElementRef: setRef
};
}
That code will work without adding key
.
Utility code for Chrome dev console to count scroll listeners:
Array.from(document.querySelectorAll('*'))
.reduce(function(pre, dom){
var clks = getEventListeners(dom).scroll;
pre += clks ? clks.length || 0 : 0;
return pre
}, 0)
Updated codesandbox: https://codesandbox.io/s/angry-einstein-6fb1u4?file=/src/App.js
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论