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

Socket.io+React/Node:如何更新排除当前连接的在线用户列表?

问题:Socket.io在线列表实时更新异常

需求:实现基础聊天功能,实时更新客户端Socket连接在线列表。示例场景:客户端一登录为x,客户端二登录为y。客户端一应在客户端二登录后看到在线列表为[y](看不到自己);客户端二应看到在线列表为[x]。
当前问题:第二个客户端连接后,双方都显示x;刷新第二个客户端页面后,双方都显示y。聊天功能未实现,卡在在线列表实时更新。


服务端代码

// Server
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
require('dotenv').config();

const app = express();
const server = http.createServer(app);

const io = new Server(server, {
  cors: {
    origin: process.env.CLIENT_ADDRESS,
    methods: ['*'],
  },
});

const PORT = process.env.SERVER_PORT;

app.use(cors());

const connections = {};

io.on('connection', (socket) => {
  console.log(`User ${socket.id} connected`);

  socket.on("store_user", (data) => {
    connections[socket.id] = {
        userName: data.userName,
        socket: socket,
      };
    
    const onlineUsers = Object.values(connections)
    .filter((user) => user.socket.id !== socket.id) 
    .map((user) => user.userName);
    
    socket.emit('get_users', onlineUsers); // Send the list only to the current user.
    socket.broadcast.emit('get_users', onlineUsers); //update everyone with the changes. 

  })

  socket.on("disconnect", () => {
    console.log(`User disconnected: ${socket.id}`);
    delete connections[socket.id];

    const onlineUsers = Object.values(connections).map((user) => user.userName);
    io.emit('get_users', onlineUsers); 

  })


});

server.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

客户端登录页面代码

import {useState} from 'react'
import { useNavigate } from 'react-router-dom';

const Login = () => {
    const [input, setInput] = useState("");
    const navigate = useNavigate();

    const setUserName = () => {
        sessionStorage.setItem("userName", input);
        navigate('/chat')
    }

  return (
    <div>
        <input type="text" onChange={(e)=>{setInput(e.target.value)}}/>
        <button onClick={setUserName}>login</button>
    </div>
  )
}

export default Login

客户端聊天页面代码

// Client
import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';

const serverURL = 'http://localhost:8000/';
const socket = io.connect(serverURL);

const Chat = () => {
  const sessionUserName = sessionStorage.getItem('userName');
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const handleDisconnect = () => {
      console.log('Disconnected');
    };

    const handleGetUsers = (data) => {
      setUsers(data);
    };

    socket.on('disconnect', handleDisconnect);
    socket.emit('store_user', { userName: sessionUserName });
    socket.on('get_users', handleGetUsers);

  }, [sessionUserName, socket]);

  return (
    <>
      <button onClick={() => console.log(users)}>Show users</button>
      <ul>
        {users.map((user) => (
          <li key={user}>{user}</li>
        ))}
      </ul>
    </>
  );
};

export default Chat;

问题原因与修复方案

核心问题

  1. 广播列表逻辑错误:新用户加入时,你给所有在线用户广播的是「过滤掉新用户的列表」,导致老用户看不到新用户。比如x先登录,y加入后,服务端给x发送的列表是[x](过滤掉y),而不是[y]。
  2. 客户端全局socket实例重复监听:socket实例在组件外全局声明,每次组件挂载都会重复绑定事件,导致状态更新混乱。
  3. 断开连接时未过滤用户自身:断开连接后给所有用户发送的列表未过滤自身,会导致用户看到自己。

修复步骤

服务端修改

调整store_user和disconnect事件的列表生成逻辑,发送完整用户列表,让客户端自己过滤掉自身:

io.on('connection', (socket) => {
  console.log(`User ${socket.id} connected`);

  socket.on("store_user", (data) => {
    // 简化connections存储,只存用户名
    connections[socket.id] = data.userName;
    
    // 获取所有在线用户的完整列表
    const allUsers = Object.values(connections);
    
    // 给当前用户发送过滤掉自己的列表
    socket.emit('get_users', allUsers.filter(name => name !== data.userName));
    // 给其他用户发送完整列表,由客户端自行过滤
    socket.broadcast.emit('get_users', allUsers);
  })

  socket.on("disconnect", () => {
    console.log(`User disconnected: ${socket.id}`);
    delete connections[socket.id];

    const allUsers = Object.values(connections);
    // 给所有用户发送完整列表,客户端自行过滤
    io.emit('get_users', allUsers); 
  })
});

客户端修改

将socket实例移到组件内部,添加事件清理逻辑,并在客户端过滤自身:

import { useEffect, useState, useRef } from 'react';
import { io } from 'socket.io-client';

const serverURL = 'http://localhost:8000/';

const Chat = () => {
  const sessionUserName = sessionStorage.getItem('userName');
  const [users, setUsers] = useState([]);
  // 用useRef保存socket实例,避免重复创建
  const socket = useRef(io.connect(serverURL)).current;

  useEffect(() => {
    const handleDisconnect = () => {
      console.log('Disconnected');
    };

    const handleGetUsers = (data) => {
      // 客户端过滤掉当前登录用户
      setUsers(data.filter(name => name !== sessionUserName));
    };

    socket.on('disconnect', handleDisconnect);
    socket.on('get_users', handleGetUsers);
    socket.emit('store_user', { userName: sessionUserName });

    // 组件卸载时移除事件监听,避免内存泄漏
    return () => {
      socket.off('disconnect', handleDisconnect);
      socket.off('get_users', handleGetUsers);
    };
  }, [sessionUserName, socket]);

  return (
    <>
      <button onClick={() => console.log(users)}>Show users</button>
      <ul>
        {users.map((user) => (
          <li key={user}>{user}</li>
        ))}
      </ul>
    </>
  );
};

export default Chat;

内容的提问来源于stack exchange,提问作者Moshe Az

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 12:35:18