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

未在useEffect清理函数调用WebSocket.close()致React应用崩溃原因问询

WebSocket未在useEffect清理中关闭导致应用崩溃的问题分析

问题描述

我正在开发一个通过WebSocket连接服务器的测试应用,当不在useEffect钩子的清理部分调用WebSocket.close()时,应用会崩溃;加上这个方法后运行正常。我知道用完后该关闭连接、清理资源,但报错提示“尝试使用一个不可用或已失效的对象”,想搞清楚问题成因。

我自己分析过,怀疑和后台运行的操作有关。我理解清理操作是在渲染后执行的,不会直接影响组件渲染前的状态或渲染过程,这个理解对吗?还是我漏了什么关键点?

错误信息

Uncaught runtime errors: ERROR An attempt was made to use an object that is not, or is no longer, usable ./src/CommunicationComponent.jsx/WebSocketComponent/
</socketRef.current.onopen@http://localhost:3000/static/js/bundle.js:191:27 EventHandlerNonNull*./src/CommunicationComponent.jsx/WebSocketComponent/<@http://localhost:3000/static/js/bundle.js:189:7 commitHookEffectListMount@http://localhost:3000/static/js/bundle.js:26115:30
  commitPassiveMountOnFiber@http://localhost:3000/static/js/bundle.js:27608:42 commitPassiveMountEffects_complete@http://localhost:3000/static/js/bundle.js:27580:38 commitPassiveMountEffects_begin@http://localhost:3000/static/js/bundle.js:27570:45 commitPassiveMountEffects@http://localhost:3000/static/js/bundle.js:27560:38
  flushPassiveEffectsImpl@http://localhost:3000/static/js/bundle.js:29443:32 flushPassiveEffects@http://localhost:3000/static/js/bundle.js:29396:18 ./node_modules/react-dom/cjs/react-dom.development.js/commitRootImpl/<@http://localhost:3000/static/js/bundle.js:29211:13
  workLoop@http://localhost:3000/static/js/bundle.js:36363:46 flushWork@http://localhost:3000/static/js/bundle.js:36341:18 performWorkUntilDeadline@http://localhost:3000/static/js/bundle.js:36578:25 EventHandlerNonNull*./node_modules/scheduler/cjs/scheduler.development.js/<@http://localhost:3000/static/js/bundle.js:36614:7
  ./node_modules/scheduler/cjs/scheduler.development.js@http://localhost:3000/static/js/bundle.js:36664:5 options.factory@http://localhost:3000/static/js/bundle.js:41901:31 __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21
  ./node_modules/scheduler/index.js@http://localhost:3000/static/js/bundle.js:36679:20 options.factory@http://localhost:3000/static/js/bundle.js:41901:31 __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21
  ./node_modules/react-dom/cjs/react-dom.development.js/<@http://localhost:3000/static/js/bundle.js:6083:40 ./node_modules/react-dom/cjs/react-dom.development.js@http://localhost:3000/static/js/bundle.js:31842:5 options.factory@http://localhost:3000/static/js/bundle.js:41901:31
  __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21 ./node_modules/react-dom/index.js@http://localhost:3000/static/js/bundle.js:31913:20 options.factory@http://localhost:3000/static/js/bundle.js:41901:31
  __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21 ./node_modules/react-dom/client.js@http://localhost:3000/static/js/bundle.js:31856:28 options.factory@http://localhost:3000/static/js/bundle.js:41901:31
  __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21 ./src/index.js@http://localhost:3000/static/js/bundle.js:298:93 options.factory@http://localhost:3000/static/js/bundle.js:41901:31
  __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 @http://localhost:3000/static/js/bundle.js:42544:56 @http://localhost:3000/static/js/bundle.js:42546:12

客户端代码

import React, { useEffect, useRef } from 'react';

export default function WebSocketComponent({ children }) {
  const socketRef = useRef(null);
  const isEstablish = useRef(false);  // Tracks if the WebSocket connection is established
  const inputRef = useRef(null);  // Ref for the input field

  useEffect(() => {
    if(!isEstablish.current)
    {
        socketRef.current = new WebSocket('ws://localhost:8080');

        socketRef.current.onopen = () => {
        console.log('WebSocket connection established.');
        socketRef.current.send('Hello Server!');
        isEstablish.current = true;  // Mark connection as established
        };

        socketRef.current.onmessage = (event) => {
        console.log('Message from server:', event.data);
        };

        socketRef.current.onerror = (error) => {
        console.error('WebSocket error:', error);
        };

        socketRef.current.onclose = () => {
        console.log('WebSocket connection closed.');
        isEstablish.current = false;  // Mark connection as closed
        };
    }
    // Cleanup function 
    return () => {
      if (socketRef.current) {
        socketRef.current.close(); //it must be here, return a error in other case
        isEstablish.current = false;
      }
    };
  }, []); 

  // Function to send data to the server
  const sendToServer = () => {

    if (isEstablish.current && socketRef.current.readyState === WebSocket.OPEN) {
      const message = inputRef.current.value;
      console.log('Sent to server:', message);
      socketRef.current.send(message);  // Send message only if connection is open
    } else {
      console.error('WebSocket is not open. Cannot send message.');
    }
  };

  return (
    <div>
      <h1>WebSocket Example</h1>
      <input ref={inputRef} />
      <button onClick={sendToServer}>Send</button>
    </div>
  );
}

问题成因与解答

1. 核心原因:WebSocket异步事件与组件生命周期脱节

当组件卸载(比如路由切换、组件被移除DOM)时,如果没主动关闭WebSocket,连接会在后台持续存在。但此时组件的socketRef虽然还持有对象引用,该WebSocket对象可能已经因为浏览器回收、连接超时或服务器主动断开变为无效状态。当WebSocket的异步回调(比如onopen、onmessage)触发时,尝试调用这个失效对象的方法,就会抛出“不可用或已失效的对象”错误。

而在清理函数中调用close(),会主动终止WebSocket连接,同时触发onclose回调,避免后续异步事件触发时访问无效对象。

2. 关于清理函数执行时机的纠正

你对清理函数执行时机的理解有误:React中useEffect的清理函数并非只在“渲染后”执行,它有两个关键触发时机:

  • 组件卸载时,必然执行清理函数
  • 当useEffect的依赖项变化,重新运行effect前,会先执行上一次effect的清理函数

这里的关键是:组件卸载后,WebSocket的异步回调依然可能触发,此时组件已不存在,socketRef指向的对象状态不可控,调用其方法自然会报错。

3. 代码中的潜在风险

你用isEstablish标记连接状态,但如果组件卸载后WebSocket的onopen才触发,isEstablish.current会被设为true,但此时组件已不在DOM中,若后续误触发sendToServer,就会访问无效的socketRef.current。清理函数里关闭连接并重置isEstablish,能从根源避免这种情况。

优化建议

  • 新增isMounted ref,在WebSocket的所有异步回调中先检查组件是否仍挂载,避免执行无效操作:
    const isMounted = useRef(true);
    useEffect(() => {
      return () => {
        isMounted.current = false;
      };
    }, []);
    // 在onopen中:
    socketRef.current.onopen = () => {
      if (!isMounted.current) return;
      // 后续操作
    };
    
  • 不要仅依赖isEstablish判断连接状态,每次发送消息前必须检查socketRef.current.readyState === WebSocket.OPEN,确保连接有效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 07:09:58