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

React中更新键对应数组的对象状态时,如何避免突变并解决消息重复显示问题?

React中更新键对应数组的对象状态时,如何避免突变并解决消息重复显示问题?

首先先给你吃个定心丸:你当前更新conversations状态的代码没有突变问题!你通过展开旧数组创建新数组、展开旧对象返回新对象的方式,完全符合React的不可变更新原则,这部分写法是对的。

那为什么消息会显示两次?问题出在你的socket事件订阅逻辑上——你的useSocketEventListener钩子中的useEffect没有清理已绑定的socket监听器,当组件因为依赖项(比如socket或peer)变化而重新渲染时,会重复绑定receiveMessage等事件监听器,导致同一条消息触发多次状态更新,最终显示重复的消息。

解决方案:给useEffect添加清理函数

你需要在useEffect的返回值中移除所有绑定的socket事件监听器,避免重复订阅。修改后的代码如下:

export const useSocketEventListener = (
  socket,
  peer,
  setPeersOnConference,
  setPeerIdsOnConference,
  setAvailableUsers,
  availableRooms,
  setAvailableRooms,
  setConferenceId,
  setCallOthersTriggered,
  setTransited,
  calls,
  setCalls,
  setConversations
) => {
  useEffect(() => {
    // 单独定义事件处理函数,方便后续移除监听
    const handleReceiveMessage = (msg, from, room) => {
      setConversations((prev) => {
        const oldConversation = room in prev ? prev[room] : [];
        const newConversation = [
          ...oldConversation,
          {
            sender: from,
            message: msg,
            time: new Date().toLocaleString(),
          },
        ];
        console.log('calling once');
        return { ...prev, [room]: newConversation };
      });
      console.log('i fire once');
    };

    const handleJoinRoomAlert = (socketId, room) => {
      // 你的原有逻辑
    };

    const handleLeaveRoomAlert = (socketId, room) => {
      alert(`${socketId} left the room ${room}`);
    };

    // 绑定socket事件
    socket?.on('receiveMessage', handleReceiveMessage);
    socket?.on('joinRoomAlert', handleJoinRoomAlert);
    socket?.on('leaveRoomAlert', handleLeaveRoomAlert);
    // ... 其他socket事件绑定保持不变

    socket?.emit('fetchData');

    // peer的监听逻辑保持不变
    peer?.on('call', async (call) => {
      try {
        setTransited(true);
        const selfStream = await getMedia();
        console.log(peer.id, selfStream);
        setPeersOnConference((prev) => ({ ...prev, [peer.id]: selfStream }));
        call.answer(selfStream);
        call.on('stream', (remoteStream) => {
          console.log(call.peer, remoteStream);
          setPeersOnConference((prev) => ({
            ...prev,
            [call.peer]: remoteStream,
          }));
        });
        call.on('close', () => {
          selfStream.getTracks().forEach((track) => track.stop());
        });
        call.on('error', (e) => console.log('error in peer call'));
      } catch (e) {
        console.log('error while receiving call');
      }
    });

    // 清理函数:移除所有绑定的事件监听器
    return () => {
      socket?.off('receiveMessage', handleReceiveMessage);
      socket?.off('joinRoomAlert', handleJoinRoomAlert);
      socket?.off('leaveRoomAlert', handleLeaveRoomAlert);
      // ... 对应移除其他socket事件监听
      
      // 同时清理peer的call监听
      peer?.off('call');
    };
  }, [socket, peer, setConversations, setPeersOnConference, setAvailableUsers, setAvailableRooms, setConferenceId, setCallOthersTriggered, setTransited, calls, setCalls]);
};

为什么这样能解决问题?

当组件卸载或者useEffect的依赖项变化时,返回的清理函数会自动执行,移除之前绑定的所有socket事件监听器,这样就不会出现同一个事件被多次监听、多次触发状态更新的情况了。

另外要注意useEffect的依赖数组:把所有在effect内部用到的外部变量(包括各个状态更新函数)都加进去,避免闭包导致的旧值问题——虽然React保证状态更新函数是稳定的,但完整添加依赖能让代码更严谨,也符合ESLint的规范。

额外验证

修改后你可以测试发送一条消息,看看console.log('calling once')是不是只打印一次,如果是,就说明重复触发的问题已经解决了。

备注:内容来源于stack exchange,提问作者Rafi Sakib

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.17 12:43:09