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

React中useEffect内声明的Socket无法在其他useEffect调用的问题

Socket.io聊天应用的状态监听问题解决

问题描述

开发Socket.io聊天应用时遇到两个矛盾问题:

  • 若将socket实例声明在useEffect外部,会导致socket重复初始化
  • 若将socket声明在useEffect内部,其他useEffect中调用socket会触发报错:Uncaught TypeError: Cannot read properties of null (reading 'on')

原代码示例

const[socket,setSocket]=useState(null)
const[records,setRecords]=useState([])

useEffect(()=> {
            axios
            .get('/api/messages/',{headers:{"auth-token":localStorage.getItem('jwtToken')}})
                .then(res => {
                    setRecords(res.data)
                    console.log(records)
                })
                .catch(err =>
                    console.log(err)      
                )

                    // main socket       
    const newsocket =io.connect(CONNECTION_PORT)

    newsocket.on('connect', msg => {
        setSocket(newsocket)
        console.log(`Admin ${ADMIN.id} is waiting. socketID: ${newsocket.id}`)
     });

      newsocket.emit('waiting', ADMIN.id);

      return(()=>{
        console.log(`admin: ${ADMIN.id} --- socketid ${newsocket.id} deleted`)
        newsocket.close()
    });

    },[]);

useEffect(()=>{

      socket.on("socketListening", (msg) => { //Error Occurs here !!!!
          console.log(msg)
      
          try{
                  const result = records.find(e=>e.id===msg.conversationId)
                  console.log(result)
                  const resultMessages=result.Messages;      
                  const newMessages =[msg,...resultMessages]
                  result.Messages=newMessages;
                  result.lastMessage=msg.message;
                  if(msg.senderId!==ADMIN.id){
                      result.Recipient=false;
                    }else{
                      result.Recipient=true;

                    }
                  console.log(result);
                  const arraywithoutrecord = records.filter(e=>e.id!==msg.conversationId)
                  setRecords([result,...arraywithoutrecord])
          }catch(e){
              console.log(e)
          }
      })

    },[records])

问题原因

  1. 初始渲染时socket状态为null,第二个useEffect依赖records执行时,直接调用socket.on会触发空指针错误
  2. 第二个useEffect未将socket加入依赖,即使后续socket初始化完成,也不会重新执行绑定监听

解决方案一:调整useEffect依赖与空值判断

通过给第二个useEffect添加socket依赖,增加空值判断,同时添加监听清理逻辑避免重复绑定:

const [socket, setSocket] = useState(null);
const [records, setRecords] = useState([]);

useEffect(() => {
  axios.get('/api/messages/', {
    headers: {"auth-token": localStorage.getItem('jwtToken')}
  })
  .then(res => {
    setRecords(res.data);
  })
  .catch(err => console.log(err));

  const newsocket = io.connect(CONNECTION_PORT);

  newsocket.on('connect', () => {
    setSocket(newsocket);
    console.log(`Admin ${ADMIN.id} 已连接。socketID: ${newsocket.id}`);
  });

  newsocket.emit('waiting', ADMIN.id);

  return () => {
    console.log(`admin: ${ADMIN.id} --- socketid ${newsocket.id} 已断开`);
    newsocket.close();
  };
}, []);

useEffect(() => {
  // 先判断socket实例是否存在,避免空指针错误
  if (!socket) return;

  // 定义监听回调函数
  const handleSocketMessage = (msg) => {
    console.log(msg);
    try {
      const result = records.find(e => e.id === msg.conversationId);
      if (!result) return; // 找不到对应会话直接返回

      // 不直接修改原对象,创建新对象保证React能检测到状态变化
      const updatedResult = {
        ...result,
        Messages: [msg, ...result.Messages],
        lastMessage: msg.message,
        Recipient: msg.senderId === ADMIN.id
      };

      const arrayWithoutRecord = records.filter(e => e.id !== msg.conversationId);
      setRecords([updatedResult, ...arrayWithoutRecord]);
    } catch (e) {
      console.log(e);
    }
  };

  // 绑定socket监听
  socket.on("socketListening", handleSocketMessage);

  // 清理函数:移除监听,防止重复绑定和内存泄漏
  return () => {
    socket.off("socketListening", handleSocketMessage);
  };
}, [socket, records]); // 依赖socket和records,状态变化时重新绑定

解决方案二:统一socket逻辑到单个useEffect(利用useRef解决闭包问题)

将所有socket相关逻辑放在同一个useEffect中,用useRef保存最新的records状态,避免闭包导致的旧状态问题:

const [socket, setSocket] = useState(null);
const [records, setRecords] = useState([]);
// 用ref保存最新的records,避免闭包访问旧状态
const recordsRef = useRef(records);

// 同步records状态到ref
useEffect(() => {
  recordsRef.current = records;
}, [records]);

useEffect(() => {
  axios.get('/api/messages/', {
    headers: {"auth-token": localStorage.getItem('jwtToken')}
  })
  .then(res => {
    setRecords(res.data);
    recordsRef.current = res.data;
  })
  .catch(err => console.log(err));

  const newsocket = io.connect(CONNECTION_PORT);

  newsocket.on('connect', () => {
    setSocket(newsocket);
    console.log(`Admin ${ADMIN.id} 已连接。socketID: ${newsocket.id}`);
  });

  newsocket.emit('waiting', ADMIN.id);

  // 直接在当前effect绑定监听,通过ref获取最新records
  const handleSocketMessage = (msg) => {
    console.log(msg);
    try {
      const currentRecords = recordsRef.current;
      const result = currentRecords.find(e => e.id === msg.conversationId);
      if (!result) return;

      const updatedResult = {
        ...result,
        Messages: [msg, ...result.Messages],
        lastMessage: msg.message,
        Recipient: msg.senderId === ADMIN.id
      };

      const arrayWithoutRecord = currentRecords.filter(e => e.id !== msg.conversationId);
      setRecords([updatedResult, ...arrayWithoutRecord]);
    } catch (e) {
      console.log(e);
    }
  };

  newsocket.on("socketListening", handleSocketMessage);

  return () => {
    console.log(`admin: ${ADMIN.id} --- socketid ${newsocket.id} 已断开`);
    newsocket.off("socketListening", handleSocketMessage);
    newsocket.close();
  };
}, []);

关键修改点说明

  • 增加socket空值判断,避免初始状态调用方法报错
  • 添加监听清理函数,使用off移除监听,防止重复绑定和内存泄漏
  • 状态更新时创建新对象/数组,保证React能正确触发重渲染
  • 利用useRef解决闭包中访问旧状态的问题(方案二)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 03:35:25