React严格模式下WebSocket更新组件时prevNodes异常问题求助
我正在开发一个使用WebSocket实现实时通信的React组件,WebSocket连接在依赖system.id的useEffect钩子中建立,system.id预计很少变化。组件简化代码如下:
export function Playground({ system }) { const [nodes, setNodes] = useState(system.nodes); const [edges, setEdges] = useState(system.edges); useEffect(() => { const ws = new WebSocket(`ws://localhost:8080/ws/${system.id}`); ws.onopen = () => { console.log('WebSocket connection opened:', system.id); } ws.onmessage = (event) => { const messageData = JSON.parse(event.data); console.log('WebSocket message received:', messageData); setNodes((prevNodes) => { let newNodes = prevNodes.map(node => { if (node.id === messageData.nodeId) { return { ...node, data: { ...node.data, metrics: messageData.metrics, } }; } else { return node; } }); return newNodes; }); } return () => { if (ws.readyState === WebSocket.OPEN) { ws.close(); } } }, [system.id]); }
但在React严格模式下运行时遇到问题:Playground组件多次渲染,控制台打印两次“WebSocket connection opened: systemID1”,之后打印一次“WebSocket connection opened: systemID2”。当收到WebSocket消息时,setNodes更新函数中的prevNodes变量仍保留systemID1的节点,即使已建立systemID2的连接,导致节点更新失败。关闭严格模式则无此问题。
请问为何prevNodes会保留旧系统的节点?我遗漏了什么?如何确保prevNodes反映当前系统ID的节点?
原因分析
严格模式的双重执行与闭包陷阱
React严格模式在开发环境中会重复执行组件渲染、useEffect等生命周期逻辑,用于检测潜在的代码问题。第一次执行useEffect后会立即调用清理函数关闭WebSocket,第二次执行会重新建立连接。当system.id从systemID1切换到systemID2时,旧的(systemID1的第二次useEffect创建的)WebSocket连接的onmessage回调,由于闭包特性依然绑定着该useEffect执行时的状态环境——此时组件的nodes状态还未更新为新系统的节点数据,若旧连接在关闭前仍收到消息,就会导致setNodes使用旧的prevNodes进行更新。未同步状态与system的变化
组件初始化时useState(system.nodes)仅设置了nodes的初始值,当system整体变化(包括system.id变更时system.nodes也更新),没有任何逻辑同步更新nodes和edges状态,导致nodes始终停留在第一个system的节点数据上,即便新的WebSocket连接建立,setNodes的prevNodes自然还是旧数据。
解决方案
1. 同步状态与system的变化
添加一个专门监听system变化的useEffect,在system更新时同步刷新nodes和edges状态:
useEffect(() => { setNodes(system.nodes); setEdges(system.edges); }, [system]);
2. 完善WebSocket的清理逻辑
在useEffect的清理函数中,不仅关闭WebSocket,还要先移除onopen和onmessage回调,彻底避免旧回调在连接关闭前继续执行:
return () => { // 先移除回调,切断闭包引用 ws.onopen = null; ws.onmessage = null; // 处理CONNECTING状态的连接,避免遗漏关闭 if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); } };
3. (可选)验证消息归属的系统
如果WebSocket消息中包含systemId字段,可以在onmessage回调中先验证消息是否属于当前连接的系统,避免错误更新旧系统的节点:
ws.onmessage = (event) => { const messageData = JSON.parse(event.data); // 仅处理当前系统的消息 if (messageData.systemId !== system.id) return; setNodes((prevNodes) => { let newNodes = prevNodes.map(node => { if (node.id === messageData.nodeId) { return { ...node, data: { ...node.data, metrics: messageData.metrics, } }; } else { return node; } }); return newNodes; }); };
内容的提问来源于stack exchange,提问作者user18684514

