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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 00:23:26