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
相关产品推荐
相关产品推荐

