React函数组件onScroll事件 setState异常:列表A滚动触发失效求助
问题描述
在自定义React函数组件的onScroll事件中设置变量状态时遇到异常:滚动列表B(内联编写的组件)时,控制台日志和scroll事件触发符合预期;但滚动列表A(提取为独立函数组件MyList)时,滚动框异常,需多次尝试才能触发onScroll的处理函数。最小复现代码如下:
import React, { useEffect, useState, useMemo, useCallback } from "react"; import _ from "lodash"; import ReactDOM from "react-dom"; export function App() { const [lastScrollTime, setLastScrollTime] = useState(0); const [nodeFound, setNodeFound] = useState(false); const ref = useCallback((node) => { if (node !== null) { console.log("node found"); handleNodeRender(); } }, []); const handleEndScroll = useMemo( () => _.debounce(() => { console.log("stop"); setLastScrollTime(0); }, 5000), [] ); const handleNodeRender = useMemo( () => _.debounce(() => { console.log("render"); setNodeFound(true); }, 100), [] ); useEffect(() => { const handleScroll = () => { if (lastScrollTime === 0) { console.log("start"); setLastScrollTime(Date.now()); } console.log("Scrolling"); handleEndScroll(); }; console.log(document.querySelector("#scroll-0")); console.log(document.querySelector("#scroll-1")); if (nodeFound) { console.log("added listener"); document .querySelector("#scroll-0") .addEventListener("scroll", handleScroll); document .querySelector("#scroll-1") .addEventListener("scroll", handleScroll); } return () => { if (nodeFound) { console.log("removed listener"); document .querySelector("#scroll-0") .addEventListener("scroll", handleScroll); document .querySelector("#scroll-1") .removeEventListener("scroll", handleScroll); } }; }, [lastScrollTime, nodeFound]); useEffect(() => { if (lastScrollTime === 0) { console.log("5 seconds waited"); } }, [lastScrollTime]); const MyList = () => { return ( <li> <div> <hr /> <p>A</p> <div ref={ref} id="scroll-0" style={{ overflow: "auto", maxHeight: "70px" }} > <li>111</li> <li>222</li> <li>333</li> <li>444</li> <li>555</li> <li>666</li> </div> </div> </li> ); }; return ( <div> <ul> <MyList /> <li> <div> <hr /> <p>B</p> <div ref={ref} id="scroll-1" style={{ overflow: "auto", maxHeight: "70px" }} > <li>111</li> <li>222</li> <li>333</li> <li>444</li> <li>555</li> <li>666</li> </div> </div> </li> </ul> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
问题原因
- 内部组件重复定义:
MyList定义在App组件内部,每次App状态更新(如lastScrollTime、nodeFound变化)时,MyList会被重新定义为全新组件类型,React会卸载旧实例并挂载新实例,导致scroll-0元素频繁销毁重建,绑定的scroll事件失效。 - 清理函数错误:
useEffect清理函数中对#scroll-0执行addEventListener而非removeEventListener,事件监听未被正确移除,引发逻辑混乱。 - 防抖延迟导致状态更新不及时:
handleNodeRender防抖函数延迟设置nodeFound,使得元素挂载后事件监听绑定时机滞后。
解决方案
1. 迁移内部组件到外部
将MyList移到App组件外部,确保组件仅被定义一次,避免因App重渲染导致的组件卸载重建:
// 移到App组件外部 const MyList = ({ scrollRef }) => { return ( <li> <div> <hr /> <p>A</p> <div ref={scrollRef} id="scroll-0" style={{ overflow: "auto", maxHeight: "70px" }} > <li>111</li> <li>222</li> <li>333</li> <li>444</li> <li>555</li> <li>666</li> </div> </div> </li> ); }; export function App() { // ... 原有逻辑 }
2. 修复清理函数的事件移除错误
修正useEffect清理函数的错误操作,确保事件监听被正确移除:
useEffect(() => { const handleScroll = () => { // ... 原有逻辑 }; if (nodeFound) { console.log("added listener"); document.querySelector("#scroll-0").addEventListener("scroll", handleScroll); document.querySelector("#scroll-1").addEventListener("scroll", handleScroll); } return () => { if (nodeFound) { console.log("removed listener"); // 修正:add改为remove document.querySelector("#scroll-0").removeEventListener("scroll", handleScroll); document.querySelector("#scroll-1").removeEventListener("scroll", handleScroll); } }; }, [lastScrollTime, nodeFound]);
3. 优化ref状态更新逻辑
移除handleNodeRender防抖函数,直接在ref回调中设置nodeFound,确保元素挂载后立即绑定事件:
const ref = useCallback((node) => { if (node !== null) { console.log("node found"); setNodeFound(true); } }, []);
4. 稳定handleScroll引用
用useCallback包裹handleScroll,避免因依赖变化导致不必要的事件解绑重绑:
const handleScroll = useCallback(() => { if (lastScrollTime === 0) { console.log("start"); setLastScrollTime(Date.now()); } console.log("Scrolling"); handleEndScroll(); }, [lastScrollTime, handleEndScroll]); useEffect(() => { if (nodeFound) { console.log("added listener"); document.querySelector("#scroll-0").addEventListener("scroll", handleScroll); document.querySelector("#scroll-1").addEventListener("scroll", handleScroll); } return () => { if (nodeFound) { console.log("removed listener"); document.querySelector("#scroll-0").removeEventListener("scroll", handleScroll); document.querySelector("#scroll-1").removeEventListener("scroll", handleScroll); } }; }, [nodeFound, handleScroll]);
内容的提问来源于stack exchange,提问作者ss1319
相关产品推荐
相关产品推荐

