React中useEffect清理Socket连接遇null及Strict Mode重复连接问题
解决方案
1. 核心问题分析
- 清理时
socketRef.current为null:initSocket是异步操作,组件卸载时可能还未完成初始化,导致socketRef.current未被赋值,直接访问会报错。 - Strict Mode下重复连接:开发环境中Strict Mode会模拟组件卸载再挂载,两次执行
useEffect的初始化逻辑,若未做判断会创建多个Socket实例。
2. 代码修改方案
步骤1:添加挂载状态跟踪
用isMounted ref跟踪组件挂载状态,避免异步操作完成后对已卸载组件执行无效操作:
import { useEffect, useRef } from 'react'; // 组件内部定义 const socketRef = useRef(null); const isMounted = useRef(true);
步骤2:修改初始化逻辑,拦截重复连接
在初始化函数中先检查socketRef.current是否已存在,同时确保仅在组件挂载时赋值Socket实例:
useEffect(() => { const init = async () => { // 已有连接则直接返回,避免重复创建 if (socketRef.current) return; try { const socket = await initSocket(); // 组件已卸载则直接断开新创建的socket if (!isMounted.current) { socket.disconnect(); return; } socketRef.current = socket; // 错误处理函数 const handleErrors = (err) => { console.log("socket error", err); toast.error("Socket connection failed, try again later."); navigate("/dashboard"); }; socket.on("connect_error", handleErrors); socket.on("connect_failed", handleErrors); // 成员离开房间处理 const handleDisconnectRoom = ({ socketId, username }) => { toast.info(`${username} left the room`); setClients(prev => prev.filter(client => client.socketId !== socketId)); }; // 成员加入房间处理 const handleJoinedRoom = ({ clients, userName, socketId }) => { if (username !== userName) { toast.success(`${userName} joined the room!`); socket.emit(Actions.SYNC_CODE, { roomId, code: codeRef.current, socketId, name: "all", }); } setClients(clients); }; // 发送加入房间请求 socket.emit(Actions.JOIN, { roomId, username }); // 绑定事件监听 socket.on(Actions.JOINED, handleJoinedRoom); socket.on(Actions.DISCONNECTED, handleDisconnectRoom); } catch (e) { console.log(e); } }; init(); // 清理函数 return () => { isMounted.current = false; if (socketRef.current) { // 解绑所有自定义事件监听 socketRef.current.off(Actions.JOINED); socketRef.current.off(Actions.DISCONNECTED); socketRef.current.off("connect_error"); socketRef.current.off("connect_failed"); // 断开连接并重置ref socketRef.current.disconnect(); socketRef.current = null; } }; }, [roomId, username, navigate]); // 补充必要依赖,避免闭包导致的旧值问题
步骤3:全局单例连接(可选)
如果需要在整个应用中保持单个Socket连接,可将socketRef提升到模块级别(组件外部),实现跨组件复用:
// 模块级别变量,全局共享Socket实例 let globalSocketRef = null; // 引用计数,管理多组件使用时的连接生命周期 let socketRefCount = 0; function YourComponent() { const isMounted = useRef(true); useEffect(() => { const init = async () => { socketRefCount += 1; // 已有全局连接则直接复用 if (globalSocketRef) { socketRef.current = globalSocketRef; // 重新绑定当前组件的事件监听 globalSocketRef.on(Actions.JOINED, handleJoinedRoom); globalSocketRef.on(Actions.DISCONNECTED, handleDisconnectRoom); return; } // 创建新连接 const socket = await initSocket(); if (!isMounted.current) { socket.disconnect(); socketRefCount -= 1; return; } globalSocketRef = socket; socketRef.current = socket; // ...绑定错误监听、加入房间等逻辑(同步骤2) }; init(); return () => { isMounted.current = false; socketRefCount -= 1; // 仅当无组件使用时才断开全局连接 if (globalSocketRef && socketRefCount === 0) { globalSocketRef.off(Actions.JOINED); globalSocketRef.off(Actions.DISCONNECTED); globalSocketRef.disconnect(); globalSocketRef = null; } else if (socketRef.current) { // 仅解绑当前组件的事件监听 socketRef.current.off(Actions.JOINED); socketRef.current.off(Actions.DISCONNECTED); } }; }, [roomId, username, navigate]); }
3. 关键优化点
- 异步安全:通过
isMounted避免组件卸载后执行Socket操作,防止内存泄漏。 - 重复连接拦截:初始化前检查
socketRef状态,避免Strict Mode或重复渲染导致的多连接问题。 - 完整清理:解绑所有自定义事件监听后再断开连接,确保资源完全释放。
- 依赖补全:将
roomId、username等变量加入依赖数组,避免闭包导致的逻辑错误。
内容的提问来源于stack exchange,提问作者Nishant Sharma
相关产品推荐
相关产品推荐

