React Hooks中Socket.IO重复添加同用户名不同Socket ID用户问题
问题诊断
你的问题核心是服务端未清理断开连接的Socket记录,导致旧的Socket ID仍关联着用户名,当用户重新连接(即使组件卸载时断开了Socket),房间内的客户端列表会同时包含旧Socket和新Socket对应的同一用户名条目。此外,客户端的Socket初始化逻辑也可能存在重复连接的隐患。
解决方案
1. 服务端:添加Socket断开时的清理逻辑
在服务端的Socket连接回调中,监听disconnect事件,清理userSocketMap中的旧记录,并通知房间内其他用户更新客户端列表。
修改server.js中的代码:
io.on('connection', (socket) => { console.log('socket connected', socket.id); socket.on(ACTIONS.JOIN, ({ roomId, username }) => { userSocketMap[socket.id] = username; socket.join(roomId); const clients = getAllConnectedClients(roomId); clients.forEach(({ socketId }) => { io.to(socketId).emit(ACTIONS.JOINED, { clients, username, socketId: socket.id, }); }); }); // 新增:处理Socket断开事件 socket.on('disconnect', () => { console.log('socket disconnected', socket.id); const username = userSocketMap[socket.id]; // 清理userSocketMap中的旧记录 delete userSocketMap[socket.id]; // 获取Socket所在的所有房间(排除自身默认房间) const rooms = Array.from(socket.rooms).filter(room => room !== socket.id); rooms.forEach(roomId => { socket.leave(roomId); const updatedClients = getAllConnectedClients(roomId); // 通知房间内其他用户该用户已离开 io.to(roomId).emit(ACTIONS.DISCONNECTED, { socketId: socket.id, username }); // 同步更新所有客户端的用户列表 updatedClients.forEach(({ socketId }) => { io.to(socketId).emit(ACTIONS.JOINED, { clients: updatedClients, username, socketId: socket.id, }); }); }); }); });
2. 客户端:优化Socket初始化与依赖管理
确保useEffect的依赖项包含可能变化的参数,避免因依赖缺失导致的重复初始化或旧监听残留;同时在更新客户端列表时增加兜底去重逻辑。
修改React组件的useEffect部分:
useEffect(() => { const init = async () => { // 先断开可能存在的旧连接 if (socketRef.current) { socketRef.current.disconnect(); } socketRef.current = await initSocket(); socketRef.current.on("connect_error", (err) => handleErrors(err)); socketRef.current.on("connect_failed", (err) => handleErrors(err)); function handleErrors(e) { console.log("socket error", e); toast.error("Socket connection failed , try again later."); reactNavigator("/"); } socketRef.current.emit(ACTIONS.JOIN, { roomId, username: location.state?.username, }); // listening for joined event socketRef.current.on( ACTIONS.JOINED, ({ clients, username, socketId }) => { console.log(username); console.log(location.state?.username); console.log(socketId); if (username !== location.state?.username) { toast.success(`${username} joined the room`); console.log(`${username} joined`); } if(username===location.state?.username){ setUsernName(username); } console.log(clients); // 兜底去重:基于socketId保留唯一用户 setclients(prev => { const clientMap = new Map(); // 先保留旧列表的有效条目 prev.forEach(client => clientMap.set(client.socketId, client)); // 用新列表覆盖更新 clients.forEach(client => clientMap.set(client.socketId, client)); return Array.from(clientMap.values()); }); socketRef.current.emit(ACTIONS.SYNC_CODE, { code: codeRef.current, socketId, }); } ); // listening for disconnecting socketRef.current.on(ACTIONS.DISCONNECTED, ({ socketId, username }) => { toast.success(`${username} left the room`); setclients((prev) => { return prev.filter((client) => client.socketId !== socketId); }); }); }; init(); return () => { //cleaning when component unmount if (socketRef.current) { socketRef.current.off(ACTIONS.JOINED); socketRef.current.off(ACTIONS.DISCONNECTED); socketRef.current.disconnect(); socketRef.current = null; } }; }, [location.state, roomId, reactNavigator]); // 添加依赖项,确保参数变化时重新初始化
3. 检查initSocket实现(关键)
确保initSocket不会重复创建Socket实例,避免多连接导致的重复条目。推荐单例模式:
// socket.js import { io } from "socket.io-client"; let socketInstance = null; export const initSocket = async () => { if (socketInstance && socketInstance.connected) { return socketInstance; } socketInstance = io('/', { reconnection: false, // 根据需求调整,避免自动重连导致的多连接 }); return socketInstance; };
核心原理说明
- 服务端通过
disconnect事件清理旧Socket记录,保证userSocketMap和房间成员列表始终是最新的; - 客户端通过完善的依赖管理和兜底去重,避免因重复初始化或延迟事件导致的列表重复;
- 单例Socket实例确保同一用户在同一时刻只有一个有效连接,从根源上避免重复条目。
内容的提问来源于stack exchange,提问作者AKhil kumar Singh
相关产品推荐
相关产品推荐

