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

React组件中WebSocket场景下闭包与事件监听器的行为疑问

React组件中WebSocket事件监听器无法读取state更新值

问题代码

function SocketClient() {
 const [socket, setSocket]= React.useState(null);
  const [msg, setMsg] = React.useState([]);

  function handleMessage(event) {
    setMsg([...msg, String(event.data)]);
  }

  if(!socket){
    const newSocket = new WebSocket("ws://localhost:8000/ws");
    newSocket.addEventListener("open", function(){
    newSocket.send("Hello Server!");
  });
    newSocket.addEventListener("message", handleMessage);
    setSocket(newSocket);
  }

  function click() {
    if (socket) {
      socket?.send("hooooooonk");
    }
  }

  return (
    <div>
      <p>Shared Component</p>
      <button onClick={click}>HONK</button>
      <pre><code>
        {JSON.stringify(msg, null, "    ")}
        </code></pre>
    </div>
  );
}

问题现象

  • 组件加载时,msg为空数组;
  • WebSocket连接建立后,open回调执行,msg被更新为["Hello Server!"]并正常显示;
  • 点击按钮发送新消息后,msg始终被新值覆盖,无法累加,最终只显示["hooooooonk"];
  • 排查发现每次handleMessage执行时,读取到的msg都是初始的空数组。

原因分析

这是闭包结合React组件渲染机制导致的问题:

  1. 组件第一次渲染时,socket为null,进入if(!socket)分支,创建WebSocket实例并绑定message监听器,此时绑定的handleMessage是当前渲染周期内定义的函数;
  2. 这个handleMessage通过闭包捕获了当前渲染周期的msg状态(空数组);
  3. 当msg更新后,组件会重新渲染,此时会生成一个新的handleMessage函数,它捕获的是更新后的msg;但WebSocket实例已经存在,不会重新进入if(!socket)分支,所以旧的handleMessage仍然绑定在WebSocket的message事件上;
  4. 后续WebSocket收到消息时,触发的是第一次渲染时绑定的旧handleMessage,它内部的msg永远是初始的空数组,因此每次更新都会用新消息覆盖,无法累加。

解决方案

方案1:使用函数式更新state

修改handleMessage,利用React的函数式更新特性,直接获取最新的state值,不依赖闭包捕获的msg:

function handleMessage(event) {
  setMsg(prevMsg => [...prevMsg, String(event.data)]);
}

函数式更新会接收当前最新的state作为参数,不管闭包里的msg是什么,都能拿到正确的最新值,从根源解决累加问题。

方案2:用useEffect管理WebSocket连接

把WebSocket的创建和监听器绑定逻辑放到useEffect中,避免在render阶段执行副作用逻辑,同时通过依赖项规范逻辑执行时机:

function SocketClient() {
  const [socket, setSocket] = React.useState(null);
  const [msg, setMsg] = React.useState([]);

  function handleMessage(event) {
    setMsg(prevMsg => [...prevMsg, String(event.data)]);
  }

  React.useEffect(() => {
    if (!socket) {
      const newSocket = new WebSocket("ws://localhost:8000/ws");
      newSocket.addEventListener("open", () => {
        newSocket.send("Hello Server!");
      });
      newSocket.addEventListener("message", handleMessage);
      
      setSocket(newSocket);

      // 组件卸载时清理WebSocket连接,避免内存泄漏
      return () => {
        newSocket.close();
      };
    }
  }, [socket]);

  function click() {
    socket?.send("hooooooonk");
  }

  return (
    <div>
      <p>Shared Component</p>
      <button onClick={click}>HONK</button>
      <pre><code>{JSON.stringify(msg, null, "    ")}</code></pre>
    </div>
  );
}

内容的提问来源于stack exchange,提问作者Pablo Barrúa Urenda

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 03:07:48