useEffect空依赖内事件监听器setState不更新问题排查
React + web3.js 事件监听场景下状态异常、日志重复问题解决方案
问题复现代码如下:
const [counter, setCounter] = useState(0); useEffect(()=> { const fetchSessions =async ()=> { instance.events.sessionCreated({fromBlock:0}).on("data",async (evt)=>{ handleSessionCreatd(evt)}) } fetchSessions(); },[]); const handleSessionCreatd =async (event)=> { let index = event.returnValues.id -1; let session = await instance.methods.sessions(index).call(); console.log("index is at:",index); setCounter(counter => counter+1); //setCounter(prev=>index); console.log("counter is at",counter); }
核心问题原因
1. counter“不更新”是假象
你在handleSessionCreatd函数里打印的counter,是当前函数闭包捕获到的当次渲染的状态初始值0。setCounter是异步触发组件重渲染的,不会同步修改当前函数作用域内的变量值,你在调用setCounter之后立刻打印,拿到的永远是闭包里存的旧值,不代表状态没更新。
要验证状态是否真的更新,直接在组件函数顶层加一句console.log('组件渲染,counter最新值:', counter),就能看到值实际是正常递增的。
你之前排除闭包问题的判断不成立:注册在web3事件上的回调永远是组件第一次渲染时生成的旧函数,这个函数的闭包里永远存着初始的counter=0,自然永远打不出更新后的值。
2. 日志重复打印两轮的两个诱因
- React18严格模式默认行为:开发环境下严格模式会对空依赖的useEffect执行「挂载→卸载→重新挂载」的流程,用来检测副作用是否正确添加清理逻辑。你的代码没有在useEffect里返回清理函数注销事件监听器,第一次执行effect注册的监听器不会被销毁,第二次执行effect时会再注册一个新的监听器,等于同时存在两个相同的监听器在接收事件。
- web3.js事件回放机制:你配置了
{fromBlock:0},每次注册监听器时web3都会从创世块开始回放所有历史的sessionCreated事件,两个监听器就会触发两次全量回放,自然会看到从index=0到index=10的日志完整重复两遍。
修复代码
const [counter, setCounter] = useState(0); // 这里打印的永远是最新的counter值,用来验证状态更新 console.log('counter最新值:', counter); useEffect(() => { let eventEmitter = null; const fetchSessions = async () => { eventEmitter = instance.events.sessionCreated({fromBlock:0}); const handleData = async (evt) => { const index = evt.returnValues.id - 1; const session = await instance.methods.sessions(index).call(); console.log("index is at:", index); // 函数式更新写法是对的,不需要依赖外部counter值 setCounter(prev => prev + 1); }; eventEmitter.on("data", handleData); // 组件卸载时移除监听器 return () => { eventEmitter.off("data", handleData); }; }; let cleanup = null; fetchSessions().then(res => cleanup = res); // useEffect返回清理函数,严格模式下卸载组件时会自动执行,注销旧监听器 return () => { cleanup?.(); }; }, []);
额外优化建议
- 如果不需要监听历史区块的事件,只需要响应监听器注册之后新产生的区块事件,把
fromBlock:0改成fromBlock: 'latest'即可,能避免每次注册监听器都拉取全量历史数据。 - 不要在事件回调、setTimeout这类脱离React渲染流程的闭包里直接打印state值判断更新状态,这类闭包会捕获注册时的旧state值,很容易出现误判。
- 不要为了避免重复执行就关闭React严格模式,正确写好副作用的清理逻辑才是根本解法,否则生产环境组件重挂载时一样会出现重复监听、内存泄漏问题。
内容的提问来源于stack exchange,提问作者ezio
相关产品推荐
相关产品推荐

