You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 17:05:56