You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 00:48:30