useEffect中调用setState导致组件重复挂载的原因及正确实现
问题原因
你的代码问题出在三个核心点,根本不存在「useEffect不能同时读写状态」的限制:
- 你把
currentData加入了注册监听器的useEffect依赖数组,每次currentData更新都会触发这个effect重新执行,你控制台看到的多次"mount dataLoaded"日志不是组件重复挂载,只是effect的重执行逻辑——组件本身没有被卸载重建,所以你写在空依赖effect里的卸载日志自然不会触发。 - 每次effect重执行都会调用
window.api.receive注册一个新的事件监听器,但你的监听器移除逻辑只在组件真正卸载时执行一次,旧的监听器从来没有被清理,会越积越多造成内存泄漏,且每个旧监听器都受JavaScript闭包限制,只能拿到它被绑定那次渲染时的currentData旧值,永远读不到最新状态。 - 你的监听器移除逻辑只传了事件名,没有传入具体的回调引用,大部分事件系统的移除逻辑是按回调引用匹配的,这种写法要么会误删同事件下的其他监听器,要么根本清不掉你绑定的对应回调。
正确实现方案
根据你的场景(监听器只需要在组件挂载时注册一次,回调里需要读取最新状态再更新),有两种成熟的实现方式:
方案1:使用函数式状态更新(推荐,最简洁)
React的setState支持传入更新函数,函数的入参就是当前最新的状态值,不需要把状态加入effect依赖就能拿到最新值,从根源上避免闭包问题,同时监听器只需要注册一次:
const [currentData, setCurrentData] = useState(null); useEffect(() => { console.log("注册dataLoaded事件监听器"); const handleDataLoaded = (receiveData) => { setCurrentData(prevData => { // prevData就是当前最新的currentData值,不存在闭包陈旧问题 console.log("读取到最新状态:", prevData); // 在这里基于prevData和接收到的receiveData计算新的状态值返回即可 return { ...prevData, receiveData }; }); }; window.api.receive("dataLoaded", handleDataLoaded); // 把清理逻辑放在当前effect的返回值中,会在组件卸载、或者effect下次重跑前自动执行 return () => { console.log("移除dataLoaded事件监听器"); // 传入具体的回调引用,精准移除当前绑定的监听器 window.api.removeListeners("dataLoaded", handleDataLoaded); }; }, []); // 空依赖,整个逻辑仅在组件挂载时执行一次
方案2:使用useRef同步最新状态(适合复杂更新逻辑)
如果你的更新逻辑比较复杂,函数式更新无法满足需求(比如需要同时读取多个最新状态、或者要在更新前做很多不涉及状态修改的逻辑),可以用useRef存储最新状态的引用,事件回调里直接读ref上的值就能拿到最新数据,同样不需要重绑监听器:
const [currentData, setCurrentData] = useState(null); // 建立ref和状态的关联 const currentDataRef = useRef(currentData); // 每次状态更新时同步ref的值 useEffect(() => { currentDataRef.current = currentData; }, [currentData]); useEffect(() => { const handleDataLoaded = (receiveData) => { // 从ref上读取永远是最新的状态值,不受闭包影响 const latestData = currentDataRef.current; console.log("读取到最新状态:", latestData); // 执行复杂逻辑后更新状态 setCurrentData(/* 计算出的新值 */); }; window.api.receive("dataLoaded", handleDataLoaded); return () => { window.api.removeListeners("dataLoaded", handleDataLoaded); }; }, []);
注意事项
- 注册全局事件、定时器、外部SDK回调这类副作用,不要把回调里用到的组件状态直接加入effect依赖数组,否则会频繁触发重绑,既影响性能也容易引入内存泄漏问题。
- 副作用的清理逻辑必须写在对应effect的返回函数里,不要拆分到单独的空依赖effect中,否则无法覆盖effect重跑时的清理场景。
内容的提问来源于stack exchange,提问作者Michael
相关产品推荐
相关产品推荐

