React 18中useEffect执行两次致Socket.IO连接失败的解决办法及原因?
问题背景
升级到React 18后,组件挂载时的空依赖useEffect会执行两次,导致Socket.IO连接失败,收到报错:
WebSocket connection to 'ws://localhost:4444/socket.io/?EIO=4&transport=websocket&sid=tvz2_YPur_k2czFbAAAo' failed: WebSocket is closed before the connection is established.
原useEffect代码如下:
useEffect(() => { dispatch(connect()); return () => dispatch(disconnect()); }, []);
测试发现:移除StrictMode后代码恢复正常;移除清理函数return () => dispatch(disconnect())也能正常运行,想了解原因和可行的解决方案。
问题原因
React 18在开发环境的StrictMode中新增了「挂载-卸载-重新挂载」的检测逻辑,目的是提前暴露组件卸载时未清理资源的问题。对于空依赖的useEffect,开发环境下会按以下流程执行:
- 首次执行effect,发起Socket连接
- 立即执行清理函数,关闭还未建立完成的连接
- 再次执行effect,重新发起连接
这就导致第一次的连接还没建立就被强制关闭,进而抛出"连接建立前已关闭"的错误。
而移除清理函数后,第一次发起的连接不会被中途终止,即使第二次effect重复调用connect,Socket.IO通常会自动复用已有连接或处理重复请求,所以不会报错,但这属于规避问题而非解决问题,生产环境虽然StrictMode会关闭,但开发环境的异常仍需处理。
可行解决方案
方案1:用Ref标记首次挂载
通过Ref记录组件是否完成首次挂载,避免重复执行连接逻辑:
import { useEffect, useRef } from 'react'; // 组件内部 const isFirstMount = useRef(true); useEffect(() => { if (isFirstMount.current) { isFirstMount.current = false; dispatch(connect()); return () => dispatch(disconnect()); } }, []);
方案2:让Socket.IO连接支持重复调用
修改connect方法的逻辑,确保多次调用时不会创建重复连接,而是复用已有实例:
// 示例:Socket.IO连接封装逻辑 let socketInstance = null; export const connect = () => { // 仅在实例不存在时创建新连接 if (!socketInstance) { socketInstance = io('ws://localhost:4444'); // 其他连接初始化逻辑 } return socketInstance; }; export const disconnect = () => { if (socketInstance) { socketInstance.disconnect(); socketInstance = null; } };
方案3:临时移除StrictMode(不推荐)
如果只是想临时绕开开发环境的检测,可以在项目入口文件中移除<StrictMode>的包裹,但这会失去React 18 StrictMode提供的其他资源泄漏检测能力,不建议长期使用。
内容的提问来源于stack exchange,提问作者Angel Hadzhiev

